From 909299725cb280bfe7cc0abec2330dd5b21aa729 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Wed, 22 Jul 2015 22:11:49 -0700 Subject: [PATCH 01/18] omaha: embed test data into the test code --- omaha/omaha_test.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/omaha/omaha_test.go b/omaha/omaha_test.go index 5a3eae4..c2f14b7 100644 --- a/omaha/omaha_test.go +++ b/omaha/omaha_test.go @@ -3,22 +3,23 @@ package omaha import ( "encoding/xml" "fmt" - "io/ioutil" - "os" "testing" ) +const SampleRequest = ` + + + + + + + + +` + 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) + xml.Unmarshal([]byte(SampleRequest), &v) if v.Os.Version != "Indy" { t.Error("Unexpected version", v.Os.Version) From 8650026537a6ffe1bae05053462a73ef2d689c80 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Wed, 22 Jul 2015 22:31:47 -0700 Subject: [PATCH 02/18] omaha: update file header style and file names --- omaha/{omaha.go => protocol.go} | 29 ++++++++++++++++------- omaha/{omaha_test.go => protocol_test.go} | 14 +++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) rename omaha/{omaha.go => protocol.go} (88%) rename omaha/{omaha_test.go => protocol_test.go} (87%) diff --git a/omaha/omaha.go b/omaha/protocol.go similarity index 88% rename from omaha/omaha.go rename to omaha/protocol.go index 8e06aef..eb6ddf4 100644 --- a/omaha/omaha.go +++ b/omaha/protocol.go @@ -1,10 +1,25 @@ -/* - Implements the Google omaha protocol. +// 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. - Omaha is a request/response protocol using XML. Requests are made by - clients and responses are given by the Omaha server. - https://github.com/google/omaha/blob/wiki/ServerProtocol.md -*/ +// 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/wiki/ServerProtocol.md package omaha import ( @@ -40,8 +55,6 @@ func (r *Request) AddApp(id string, version string) *App { return a } -/* Response - */ type Response struct { XMLName xml.Name `xml:"response" datastore:"-" json:"-"` DayStart DayStart `xml:"daystart"` diff --git a/omaha/omaha_test.go b/omaha/protocol_test.go similarity index 87% rename from omaha/omaha_test.go rename to omaha/protocol_test.go index c2f14b7..eb6c54c 100644 --- a/omaha/omaha_test.go +++ b/omaha/protocol_test.go @@ -1,3 +1,17 @@ +// 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 ( From 6ab36bd0dc026c051fcd26f75bf7dbbddb39cda2 Mon Sep 17 00:00:00 2001 From: Nick Owens Date: Mon, 16 Nov 2015 12:53:45 -0800 Subject: [PATCH 03/18] 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 --- omaha/protocol_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/omaha/protocol_test.go b/omaha/protocol_test.go index eb6c54c..cc0ff25 100644 --- a/omaha/protocol_test.go +++ b/omaha/protocol_test.go @@ -76,7 +76,7 @@ func TestOmahaRequestUpdateCheck(t *testing.T) { } } -func ExampleOmaha_NewResponse() { +func ExampleNewResponse() { response := NewResponse("unit-test") app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}") app.Status = "ok" @@ -124,7 +124,7 @@ func ExampleOmaha_NewResponse() { // } -func ExampleOmaha_NewRequest() { +func ExampleNewRequest() { request := NewRequest("Indy", "Chrome OS", "ForcedUpdate_x86_64", "") app := request.AddApp("{27BD862E-8AE8-4886-A055-F7F1A6460627}", "1.0.0.0") app.AddUpdateCheck() From fef283aeb62a8fbc289b1744b12ebd49f1ae5c72 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Wed, 22 Jul 2015 23:17:32 -0700 Subject: [PATCH 04/18] 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. --- omaha/protocol.go | 50 ++++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/omaha/protocol.go b/omaha/protocol.go index eb6ddf4..347254c 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -27,7 +27,7 @@ import ( ) type Request struct { - XMLName xml.Name `xml:"request" datastore:"-"` + XMLName xml.Name `xml:"request" json:"-"` Os Os `xml:"os"` Apps []*App `xml:"app"` Protocol string `xml:"protocol,attr"` @@ -56,7 +56,7 @@ func (r *Request) AddApp(id string, version string) *App { } type Response struct { - XMLName xml.Name `xml:"response" datastore:"-" json:"-"` + XMLName xml.Name `xml:"response" json:"-"` DayStart DayStart `xml:"daystart"` Apps []*App `xml:"app"` Protocol string `xml:"protocol,attr"` @@ -80,7 +80,6 @@ func (r *Response) AddApp(id string) *App { } type App struct { - XMLName xml.Name `xml:"app" datastore"-" json:"-"` Ping *Ping `xml:"ping"` UpdateCheck *UpdateCheck `xml:"updatecheck"` Events []*Event `xml:"event" json:",omitempty"` @@ -124,7 +123,6 @@ func (a *App) AddEvent() *Event { } type UpdateCheck struct { - XMLName xml.Name `xml:"updatecheck" datastore:"-" json:"-"` Urls *Urls `xml:"urls"` Manifest *Manifest `xml:"manifest"` TargetVersionPrefix string `xml:"targetversionprefix,attr,omitempty"` @@ -147,17 +145,15 @@ func (u *UpdateCheck) AddManifest(version string) *Manifest { } type Ping struct { - XMLName xml.Name `xml:"ping" datastore:"-" json:"-"` - LastReportDays string `xml:"r,attr,omitempty"` - Status string `xml:"status,attr,omitempty"` + LastReportDays string `xml:"r,attr,omitempty"` + Status string `xml:"status,attr,omitempty"` } type Os struct { - XMLName xml.Name `xml:"os" datastore:"-" json:"-"` - Platform string `xml:"platform,attr,omitempty"` - Version string `xml:"version,attr,omitempty"` - Sp string `xml:"sp,attr,omitempty"` - Arch string `xml:"arch,attr,omitempty"` + 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 { @@ -166,41 +162,35 @@ func NewOs(platform string, version string, sp string, arch string) *Os { } type Event struct { - XMLName xml.Name `xml:"event" datastore:"-" json:"-"` - Type string `xml:"eventtype,attr,omitempty"` - Result string `xml:"eventresult,attr,omitempty"` - PreviousVersion string `xml:"previousversion,attr,omitempty"` - ErrorCode string `xml:"errorcode,attr,omitempty"` + Type string `xml:"eventtype,attr,omitempty"` + Result string `xml:"eventresult,attr,omitempty"` + PreviousVersion string `xml:"previousversion,attr,omitempty"` + ErrorCode string `xml:"errorcode,attr,omitempty"` } type Urls struct { - XMLName xml.Name `xml:"urls" datastore:"-" json:"-"` - Urls []Url `xml:"url" json:",omitempty"` + Urls []Url `xml:"url" json:",omitempty"` } type Url struct { - XMLName xml.Name `xml:"url" datastore:"-" json:"-"` - CodeBase string `xml:"codebase,attr"` + CodeBase string `xml:"codebase,attr"` } type Manifest struct { - XMLName xml.Name `xml:"manifest" datastore:"-" json:"-"` Packages Packages `xml:"packages"` Actions Actions `xml:"actions"` Version string `xml:"version,attr"` } type Packages struct { - XMLName xml.Name `xml:"packages" datastore:"-" json:"-"` Packages []Package `xml:"package" json:",omitempty"` } type Package struct { - XMLName xml.Name `xml:"package" datastore:"-" json:"-"` - Hash string `xml:"hash,attr"` - Name string `xml:"name,attr"` - Size string `xml:"size,attr"` - Required bool `xml:"required,attr"` + 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 { @@ -210,13 +200,11 @@ func (m *Manifest) AddPackage(hash string, name string, size string, required bo } type Actions struct { - XMLName xml.Name `xml:"actions" datastore:"-" json:"-"` Actions []*Action `xml:"action" json:",omitempty"` } type Action struct { - XMLName xml.Name `xml:"action" datastore:"-" json:"-"` - Event string `xml:"event,attr"` + Event string `xml:"event,attr"` // Extensions added by update_engine ChromeOSVersion string `xml:"ChromeOSVersion,attr"` From ec70842bdd9dfb4e1074746ddb46945ce1996f95 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Thu, 23 Jul 2015 23:14:36 -0700 Subject: [PATCH 05/18] 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. --- omaha/codes.go | 170 +++++++++++++++++++++++++++++++++++++++++ omaha/protocol.go | 167 ++++++++++++++-------------------------- omaha/protocol_test.go | 39 ++++++---- omaha/system.go | 53 +++++++++++++ 4 files changed, 308 insertions(+), 121 deletions(-) create mode 100644 omaha/codes.go create mode 100644 omaha/system.go diff --git a/omaha/codes.go b/omaha/codes.go new file mode 100644 index 0000000..8eefeb2 --- /dev/null +++ b/omaha/codes.go @@ -0,0 +1,170 @@ +// 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" +) + +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" +) diff --git a/omaha/protocol.go b/omaha/protocol.go index 347254c..c4af2eb 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -24,11 +24,13 @@ package omaha import ( "encoding/xml" + + "github.com/coreos/mantle/version" ) type Request struct { XMLName xml.Name `xml:"request" json:"-"` - Os Os `xml:"os"` + OS *OS `xml:"os"` Apps []*App `xml:"app"` Protocol string `xml:"protocol,attr"` Version string `xml:"version,attr,omitempty"` @@ -41,16 +43,20 @@ type Request struct { 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 NewRequest() *Request { + return &Request{ + Protocol: "3.0", + Version: version.Version, + OS: &OS{ + Platform: LocalPlatform(), + Arch: LocalArch(), + // TODO(marineam): Version and ServicePack + }, + } } -func (r *Request) AddApp(id string, version string) *App { - a := NewApp(id) - a.Version = version +func (r *Request) AddApp(id, version string) *App { + a := &App{Id: id, Version: version} r.Apps = append(r.Apps, a) return a } @@ -63,18 +69,20 @@ type Response struct { Server string `xml:"server,attr"` } -func NewResponse(server string) *Response { - r := &Response{Server: server, Protocol: "3.0"} - r.DayStart.ElapsedSeconds = "0" - return r +func NewResponse() *Response { + return &Response{ + Protocol: "3.0", + Server: "mantle", + DayStart: DayStart{ElapsedSeconds: "0"}, + } } type DayStart struct { ElapsedSeconds string `xml:"elapsed_seconds,attr"` } -func (r *Response) AddApp(id string) *App { - a := NewApp(id) +func (r *Response) AddApp(id string, status AppStatus) *App { + a := &App{Id: id, Status: status} r.Apps = append(r.Apps, a) return a } @@ -89,7 +97,7 @@ type App struct { Lang string `xml:"lang,attr,omitempty"` Client string `xml:"client,attr,omitempty"` InstallAge string `xml:"installage,attr,omitempty"` - Status string `xml:"status,attr,omitempty"` + Status AppStatus `xml:"status,attr,omitempty"` // update engine extensions Track string `xml:"track,attr,omitempty"` @@ -101,11 +109,6 @@ type App struct { OEM string `xml:"oem,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 @@ -123,19 +126,20 @@ func (a *App) AddEvent() *Event { } type UpdateCheck struct { - Urls *Urls `xml:"urls"` - Manifest *Manifest `xml:"manifest"` - TargetVersionPrefix string `xml:"targetversionprefix,attr,omitempty"` - Status string `xml:"status,attr,omitempty"` + URLs *URLs `xml:"urls"` + Manifest *Manifest `xml:"manifest"` + TargetVersionPrefix string `xml:"targetversionprefix,attr,omitempty"` + Status UpdateStatus `xml:"status,attr,omitempty"` } -func (u *UpdateCheck) AddUrl(codebase string) *Url { - if u.Urls == nil { - u.Urls = new(Urls) +func (u *UpdateCheck) AddURL(codebase string) *URL { + // An intermediate struct is used instead of a "urls>url" tag simply + // to keep Go from generating if the list is empty. + if u.URLs == nil { + u.URLs = new(URLs) } - url := new(Url) - url.CodeBase = codebase - u.Urls.Urls = append(u.Urls.Urls, *url) + url := &URL{CodeBase: codebase} + u.URLs.URLs = append(u.URLs.URLs, url) return url } @@ -149,58 +153,52 @@ type Ping struct { Status string `xml:"status,attr,omitempty"` } -type Os struct { - 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 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 Event struct { - Type string `xml:"eventtype,attr,omitempty"` - Result string `xml:"eventresult,attr,omitempty"` - PreviousVersion string `xml:"previousversion,attr,omitempty"` - ErrorCode string `xml:"errorcode,attr,omitempty"` + Type EventType `xml:"eventtype,attr"` + Result EventResult `xml:"eventresult,attr"` + PreviousVersion string `xml:"previousversion,attr,omitempty"` + ErrorCode string `xml:"errorcode,attr,omitempty"` + Status string `xml:"status,attr,omitempty"` } -type Urls struct { - Urls []Url `xml:"url" json:",omitempty"` +type URLs struct { + URLs []*URL `xml:"url" json:",omitempty"` } -type Url struct { +type URL struct { CodeBase string `xml:"codebase,attr"` } type Manifest struct { - Packages Packages `xml:"packages"` - Actions Actions `xml:"actions"` - Version string `xml:"version,attr"` -} - -type Packages struct { - Packages []Package `xml:"package" json:",omitempty"` + Packages []*Package `xml:"packages>package"` + Actions []*Action `xml:"actions>action"` + Version string `xml:"version,attr"` } type Package struct { Hash string `xml:"hash,attr"` Name string `xml:"name,attr"` - Size string `xml:"size,attr"` + Size uint64 `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) +func (m *Manifest) AddPackage() *Package { + p := &Package{} + m.Packages = append(m.Packages, p) return p } -type Actions struct { - Actions []*Action `xml:"action" json:",omitempty"` +func (m *Manifest) AddAction(event string) *Action { + a := &Action{Event: event} + m.Actions = append(m.Actions, a) + return a } type Action struct { @@ -216,50 +214,3 @@ type Action struct { MetadataSize string `xml:"MetadataSize,attr,omitempty"` Deadline string `xml:"deadline,attr,omitempty"` } - -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", - 800: "ping", -} - -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", -} diff --git a/omaha/protocol_test.go b/omaha/protocol_test.go index cc0ff25..428aca2 100644 --- a/omaha/protocol_test.go +++ b/omaha/protocol_test.go @@ -35,8 +35,8 @@ func TestOmahaRequestUpdateCheck(t *testing.T) { v := Request{} xml.Unmarshal([]byte(SampleRequest), &v) - if v.Os.Version != "Indy" { - t.Error("Unexpected version", v.Os.Version) + if v.OS.Version != "Indy" { + t.Error("Unexpected version", v.OS.Version) } if v.Apps[0].Id != "{87efface-864d-49a5-9bb3-4b050a7c227a}" { @@ -71,22 +71,29 @@ func TestOmahaRequestUpdateCheck(t *testing.T) { t.Error("dev-channel") } - if v.Apps[0].Events[0].Type != "3" { - t.Error("developer-build") + 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 ExampleNewResponse() { - response := NewResponse("unit-test") - app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}") - app.Status = "ok" + response := NewResponse() + app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}", "ok") p := app.AddPing() p.Status = "ok" u := app.AddUpdateCheck() u.Status = "ok" - u.AddUrl("http://localhost/updates") + u.AddURL("http://localhost/updates") m := u.AddManifest("9999.0.0") - m.AddPackage("+LXvjiaPkeYDLHoNKlf9qbJwvnk=", "update.gz", "67546213", true) + k := m.AddPackage() + k.Hash = "+LXvjiaPkeYDLHoNKlf9qbJwvnk=" + k.Name = "update.gz" + k.Size = 67546213 + k.Required = true a := m.AddAction("postinstall") a.ChromeOSVersion = "9999.0.0" a.Sha256 = "0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=" @@ -103,7 +110,7 @@ func ExampleNewResponse() { // Output: // - // + // // // // @@ -125,13 +132,19 @@ func ExampleNewResponse() { } func ExampleNewRequest() { - request := NewRequest("Indy", "Chrome OS", "ForcedUpdate_x86_64", "") + 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 = "1" - event.Result = "0" + event.Type = EventTypeDownloadComplete + event.Result = EventResultError if raw, err := xml.MarshalIndent(request, "", " "); err != nil { fmt.Println(err) diff --git a/omaha/system.go b/omaha/system.go new file mode 100644 index 0000000..1311a1c --- /dev/null +++ b/omaha/system.go @@ -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 + } +} From 75a1125f53955b690fc4d5ab2bf338376676367c Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Sat, 25 Jul 2015 16:44:18 -0700 Subject: [PATCH 06/18] 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. --- omaha/protocol.go | 156 ++++++++++++++++++++++++++--------------- omaha/protocol_test.go | 6 +- 2 files changed, 101 insertions(+), 61 deletions(-) diff --git a/omaha/protocol.go b/omaha/protocol.go index c4af2eb..267bec7 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -28,19 +28,20 @@ import ( "github.com/coreos/mantle/version" ) +// Request sent by the Omaha client type Request struct { - XMLName xml.Name `xml:"request" json:"-"` - 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"` + XMLName xml.Name `xml:"request" json:"-"` + OS *OS `xml:"os"` + Apps []*AppRequest `xml:"app"` + Protocol string `xml:"protocol,attr"` + Version string `xml:"version,attr,omitempty"` + IsMachine string `xml:"ismachine,attr,omitempty"` + RequestId string `xml:"requestid,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"` + UpdaterVersion string `xml:"updaterversion,attr,omitempty"` } func NewRequest() *Request { @@ -55,18 +56,71 @@ func NewRequest() *Request { } } -func (r *Request) AddApp(id, version string) *App { - a := &App{Id: id, Version: version} +func (r *Request) AddApp(id, version string) *AppRequest { + a := &AppRequest{Id: id, Version: version} r.Apps = append(r.Apps, a) return a } +type AppRequest struct { + Ping *PingRequest `xml:"ping"` + UpdateCheck *UpdateRequest `xml:"updatecheck"` + Events []*EventRequest `xml:"event" json:",omitempty"` + 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"` + + // update engine extensions + Track string `xml:"track,attr,omitempty"` + FromTrack string `xml:"from_track,attr,omitempty"` + + // coreos update engine extensions + BootId string `xml:"bootid,attr,omitempty"` + MachineID string `xml:"machineid,attr,omitempty"` + OEM string `xml:"oem,attr,omitempty"` +} + +func (a *AppRequest) AddUpdateCheck() *UpdateRequest { + a.UpdateCheck = &UpdateRequest{} + return a.UpdateCheck +} + +func (a *AppRequest) AddPing() *PingRequest { + a.Ping = new(PingRequest) + 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 { + LastReportDays string `xml:"r,attr,omitempty"` +} + +type EventRequest struct { + Type EventType `xml:"eventtype,attr"` + Result EventResult `xml:"eventresult,attr"` + PreviousVersion string `xml:"previousversion,attr,omitempty"` + ErrorCode string `xml:"errorcode,attr,omitempty"` +} + +// Response sent by the Omaha server type Response struct { - XMLName xml.Name `xml:"response" json:"-"` - DayStart DayStart `xml:"daystart"` - Apps []*App `xml:"app"` - Protocol string `xml:"protocol,attr"` - Server string `xml:"server,attr"` + 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 { @@ -81,58 +135,43 @@ type DayStart struct { ElapsedSeconds string `xml:"elapsed_seconds,attr"` } -func (r *Response) AddApp(id string, status AppStatus) *App { - a := &App{Id: id, Status: status} +func (r *Response) AddApp(id string, status AppStatus) *AppResponse { + a := &AppResponse{Id: id, Status: status} r.Apps = append(r.Apps, a) return a } -type App struct { - Ping *Ping `xml:"ping"` - UpdateCheck *UpdateCheck `xml:"updatecheck"` - Events []*Event `xml:"event" json:",omitempty"` - 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"` - Status AppStatus `xml:"status,attr,omitempty"` - - // update engine extensions - Track string `xml:"track,attr,omitempty"` - FromTrack string `xml:"from_track,attr,omitempty"` - - // coreos update engine extensions - BootId string `xml:"bootid,attr,omitempty"` - MachineID string `xml:"machineid,attr,omitempty"` - OEM string `xml:"oem,attr,omitempty"` +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 *App) AddUpdateCheck() *UpdateCheck { - a.UpdateCheck = new(UpdateCheck) +func (a *AppResponse) AddUpdateCheck(status UpdateStatus) *UpdateResponse { + a.UpdateCheck = &UpdateResponse{Status: status} return a.UpdateCheck } -func (a *App) AddPing() *Ping { - a.Ping = new(Ping) +func (a *AppResponse) AddPing() *PingResponse { + a.Ping = &PingResponse{"ok"} return a.Ping } -func (a *App) AddEvent() *Event { - event := new(Event) +func (a *AppResponse) AddEvent() *EventResponse { + event := &EventResponse{"ok"} a.Events = append(a.Events, event) return event } -type UpdateCheck struct { - URLs *URLs `xml:"urls"` - Manifest *Manifest `xml:"manifest"` - TargetVersionPrefix string `xml:"targetversionprefix,attr,omitempty"` - Status UpdateStatus `xml:"status,attr,omitempty"` +type UpdateResponse struct { + URLs *URLs `xml:"urls"` + Manifest *Manifest `xml:"manifest"` + Status UpdateStatus `xml:"status,attr,omitempty"` } -func (u *UpdateCheck) AddURL(codebase string) *URL { +func (u *UpdateResponse) AddURL(codebase string) *URL { // An intermediate struct is used instead of a "urls>url" tag simply // to keep Go from generating if the list is empty. if u.URLs == nil { @@ -143,14 +182,17 @@ func (u *UpdateCheck) AddURL(codebase string) *URL { return url } -func (u *UpdateCheck) AddManifest(version string) *Manifest { +func (u *UpdateResponse) AddManifest(version string) *Manifest { u.Manifest = &Manifest{Version: version} return u.Manifest } -type Ping struct { - LastReportDays string `xml:"r,attr,omitempty"` - Status string `xml:"status,attr,omitempty"` +type PingResponse struct { + Status string `xml:"status,attr"` // Always "ok". +} + +type EventResponse struct { + Status string `xml:"status,attr"` // Always "ok". } type OS struct { diff --git a/omaha/protocol_test.go b/omaha/protocol_test.go index 428aca2..df58c2e 100644 --- a/omaha/protocol_test.go +++ b/omaha/protocol_test.go @@ -83,10 +83,8 @@ func TestOmahaRequestUpdateCheck(t *testing.T) { func ExampleNewResponse() { response := NewResponse() app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}", "ok") - p := app.AddPing() - p.Status = "ok" - u := app.AddUpdateCheck() - u.Status = "ok" + app.AddPing() + u := app.AddUpdateCheck(UpdateOK) u.AddURL("http://localhost/updates") m := u.AddManifest("9999.0.0") k := m.AddPackage() From a3bc6682251c73eae20bb24e0bc9b3a362f58811 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Sat, 25 Jul 2015 18:00:13 -0700 Subject: [PATCH 07/18] omaha: add/fix missing or outdated attributes --- omaha/protocol.go | 56 +++++++++++++++++++++++++----------------- omaha/protocol_test.go | 6 ++--- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/omaha/protocol.go b/omaha/protocol.go index 267bec7..caca6c0 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -30,18 +30,20 @@ import ( // 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"` - Version string `xml:"version,attr,omitempty"` - IsMachine string `xml:"ismachine,attr,omitempty"` - RequestId string `xml:"requestid,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"` - UpdaterVersion string `xml:"updaterversion,attr,omitempty"` + XMLName xml.Name `xml:"request" json:"-"` + OS *OS `xml:"os"` + Apps []*AppRequest `xml:"app"` + Protocol string `xml:"protocol,attr"` + Version string `xml:"version,attr,omitempty"` + IsMachine string `xml:"ismachine,attr,omitempty"` + RequestId string `xml:"requestid,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"` + + // update engine extension, duplicates the version attribute. + UpdaterVersion string `xml:"updaterversion,attr,omitempty"` } func NewRequest() *Request { @@ -76,11 +78,15 @@ type AppRequest struct { // update engine extensions Track string `xml:"track,attr,omitempty"` FromTrack string `xml:"from_track,attr,omitempty"` + Board string `xml:"board,attr,omitempty"` + DeltaOK bool `xml:"delta_okay,attr,omitempty"` // coreos update engine extensions - BootId string `xml:"bootid,attr,omitempty"` - MachineID string `xml:"machineid,attr,omitempty"` - OEM string `xml:"oem,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"` + AlephVersion string `xml:"alephversion,attr,omitempty"` } func (a *AppRequest) AddUpdateCheck() *UpdateRequest { @@ -89,7 +95,7 @@ func (a *AppRequest) AddUpdateCheck() *UpdateRequest { } func (a *AppRequest) AddPing() *PingRequest { - a.Ping = new(PingRequest) + a.Ping = &PingRequest{Active: 1} return a.Ping } @@ -104,12 +110,15 @@ type UpdateRequest struct { } type PingRequest struct { - LastReportDays string `xml:"r,attr,omitempty"` + 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"` + NextVersion string `xml:"nextversion,attr,omitempty"` PreviousVersion string `xml:"previousversion,attr,omitempty"` ErrorCode string `xml:"errorcode,attr,omitempty"` } @@ -246,13 +255,16 @@ func (m *Manifest) AddAction(event string) *Action { type Action struct { Event string `xml:"event,attr"` - // Extensions added by update_engine - ChromeOSVersion string `xml:"ChromeOSVersion,attr"` - Sha256 string `xml:"sha256,attr"` - NeedsAdmin bool `xml:"needsadmin,attr"` - IsDelta bool `xml:"IsDelta,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"` } diff --git a/omaha/protocol_test.go b/omaha/protocol_test.go index df58c2e..6fb106f 100644 --- a/omaha/protocol_test.go +++ b/omaha/protocol_test.go @@ -93,10 +93,10 @@ func ExampleNewResponse() { k.Size = 67546213 k.Required = true a := m.AddAction("postinstall") - a.ChromeOSVersion = "9999.0.0" + a.DisplayVersion = "9999.0.0" a.Sha256 = "0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=" a.NeedsAdmin = false - a.IsDelta = true + a.IsDeltaPayload = true a.DisablePayloadBackoff = true if raw, err := xml.MarshalIndent(response, "", " "); err != nil { @@ -121,7 +121,7 @@ func ExampleNewResponse() { // // // - // + // // // // From f208691b12e43cd56da3e541707424274bb01050 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Sat, 8 Aug 2015 17:01:00 -0700 Subject: [PATCH 08/18] 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. --- omaha/protocol.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/omaha/protocol.go b/omaha/protocol.go index caca6c0..3abe808 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -175,19 +175,14 @@ func (a *AppResponse) AddEvent() *EventResponse { } type UpdateResponse struct { - URLs *URLs `xml:"urls"` + URLs []*URL `xml:"urls>url" json:",omitempty"` Manifest *Manifest `xml:"manifest"` Status UpdateStatus `xml:"status,attr,omitempty"` } func (u *UpdateResponse) AddURL(codebase string) *URL { - // An intermediate struct is used instead of a "urls>url" tag simply - // to keep Go from generating if the list is empty. - if u.URLs == nil { - u.URLs = new(URLs) - } url := &URL{CodeBase: codebase} - u.URLs.URLs = append(u.URLs.URLs, url) + u.URLs = append(u.URLs, url) return url } @@ -219,10 +214,6 @@ type Event struct { Status string `xml:"status,attr,omitempty"` } -type URLs struct { - URLs []*URL `xml:"url" json:",omitempty"` -} - type URL struct { CodeBase string `xml:"codebase,attr"` } From 5543f86194e1f087da5f9ff0dc4e175b7b1a8d81 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Mon, 10 Aug 2015 15:50:31 -0700 Subject: [PATCH 09/18] omaha: add helper method for computing package metadata --- omaha/package.go | 112 ++++++++++++++++++++++++++++++++ omaha/package_test.go | 144 +++++++++++++++++++++++++++++++++++++++++ omaha/protocol.go | 16 +++-- omaha/protocol_test.go | 4 +- 4 files changed, 267 insertions(+), 9 deletions(-) create mode 100644 omaha/package.go create mode 100644 omaha/package_test.go diff --git a/omaha/package.go b/omaha/package.go new file mode 100644 index 0000000..497d239 --- /dev/null +++ b/omaha/package.go @@ -0,0 +1,112 @@ +// 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. The Sha256 attribute +// is not a standard part of the Omaha protocol which only uses Sha1. +type Package struct { + Name string `xml:"name,attr"` + Sha1 string `xml:"hash,attr"` + Sha256 string `xml:"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 protocol extension. + 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 +} diff --git a/omaha/package_test.go b/omaha/package_test.go new file mode 100644 index 0000000..c617218 --- /dev/null +++ b/omaha/package_test.go @@ -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) + } +} diff --git a/omaha/protocol.go b/omaha/protocol.go index 3abe808..b7b152e 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -224,19 +224,21 @@ type Manifest struct { Version string `xml:"version,attr"` } -type Package struct { - Hash string `xml:"hash,attr"` - Name string `xml:"name,attr"` - Size uint64 `xml:"size,attr"` - Required bool `xml:"required,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) diff --git a/omaha/protocol_test.go b/omaha/protocol_test.go index 6fb106f..54b6b0f 100644 --- a/omaha/protocol_test.go +++ b/omaha/protocol_test.go @@ -88,7 +88,7 @@ func ExampleNewResponse() { u.AddURL("http://localhost/updates") m := u.AddManifest("9999.0.0") k := m.AddPackage() - k.Hash = "+LXvjiaPkeYDLHoNKlf9qbJwvnk=" + k.Sha1 = "+LXvjiaPkeYDLHoNKlf9qbJwvnk=" k.Name = "update.gz" k.Size = 67546213 k.Required = true @@ -118,7 +118,7 @@ func ExampleNewResponse() { // // // - // + // // // // From 5e54ada1e9f293855c670218451e960d2f2e1bc7 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Sun, 11 Oct 2015 13:38:48 -0700 Subject: [PATCH 10/18] 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. --- omaha/update.go | 50 ++++++++++++++++++++++++++++++++++++++++++++ omaha/update_test.go | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 omaha/update.go create mode 100644 omaha/update_test.go diff --git a/omaha/update.go b/omaha/update.go new file mode 100644 index 0000000..a430acb --- /dev/null +++ b/omaha/update.go @@ -0,0 +1,50 @@ +// 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 +} + +type Updater interface { + Update(os *OS, app *AppRequest) (*Update, error) +} diff --git a/omaha/update_test.go b/omaha/update_test.go new file mode 100644 index 0000000..d6e0bdf --- /dev/null +++ b/omaha/update_test.go @@ -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 = ` + + + + + + + + + +` + +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) + } +} From 2cf1d8f13e5ac6906339f12367c2ec233a989245 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Wed, 11 Nov 2015 15:10:30 -0800 Subject: [PATCH 11/18] omaha: support using status codes as error values --- omaha/codes.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/omaha/codes.go b/omaha/codes.go index 8eefeb2..783491a 100644 --- a/omaha/codes.go +++ b/omaha/codes.go @@ -155,8 +155,14 @@ const ( // 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 string(a) +} + type UpdateStatus string const ( @@ -168,3 +174,8 @@ const ( UpdateHashError UpdateStatus = "error-hash" UpdateInternalError UpdateStatus = "error-internal" ) + +// Make UpdateStatus easy to use as an error +func (u UpdateStatus) Error() string { + return string(u) +} From f33cb66abb1821567639ece5e718a3175ceae161 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Sat, 25 Jul 2015 19:00:42 -0700 Subject: [PATCH 12/18] omaha: add complete http handler implementation The handler is driven by something implementing the 'Updater' interface. --- omaha/handler.go | 134 ++++++++++++++++++++++++++++++++++++++++++ omaha/handler_test.go | 68 +++++++++++++++++++++ omaha/update.go | 25 +++++++- 3 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 omaha/handler.go create mode 100644 omaha/handler_test.go diff --git a/omaha/handler.go b/omaha/handler.go new file mode 100644 index 0000000..754ffc3 --- /dev/null +++ b/omaha/handler.go @@ -0,0 +1,134 @@ +// 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) + + decoder := xml.NewDecoder(reader) + var omahaReq Request + if err := decoder.Decode(&omahaReq); err != nil { + log.Printf("omaha: Failed decoding XML: %v", err) + http.Error(w, "Invalid XML", http.StatusBadRequest) + return + } + + if omahaReq.Protocol != "3.0" { + log.Printf("omaha: Unexpected protocol: %q", omahaReq.Protocol) + http.Error(w, "Omaha 3.0 Required", 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 +} diff --git a/omaha/handler_test.go b/omaha/handler_test.go new file mode 100644 index 0000000..013dea5 --- /dev/null +++ b/omaha/handler_test.go @@ -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) + } +} diff --git a/omaha/update.go b/omaha/update.go index a430acb..ce2188e 100644 --- a/omaha/update.go +++ b/omaha/update.go @@ -45,6 +45,29 @@ func (u *Update) URLs(prefixes []string) []*URL { return urls } +// Updater provides a common interface for any backend that can respond to +// update requests made to an Omaha server. type Updater interface { - Update(os *OS, app *AppRequest) (*Update, error) + 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 } From e5eb9eb58328a79df8b4d8e9b0a2c62be1d24b2c Mon Sep 17 00:00:00 2001 From: Nick Owens Date: Tue, 22 Dec 2015 14:16:35 -0800 Subject: [PATCH 13/18] 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. --- omaha/isclosed.go | 32 ++++++++++++ omaha/server.go | 73 +++++++++++++++++++++++++++ omaha/server_test.go | 114 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 omaha/isclosed.go create mode 100644 omaha/server.go create mode 100644 omaha/server_test.go diff --git a/omaha/isclosed.go b/omaha/isclosed.go new file mode 100644 index 0000000..5b00698 --- /dev/null +++ b/omaha/isclosed.go @@ -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" +} diff --git a/omaha/server.go b/omaha/server.go new file mode 100644 index 0000000..dcf025a --- /dev/null +++ b/omaha/server.go @@ -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() +} diff --git a/omaha/server_test.go b/omaha/server_test.go new file mode 100644 index 0000000..d5806bc --- /dev/null +++ b/omaha/server_test.go @@ -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) + } +} From 4d02220019dbe73903046e7ceeb0038f81c795a1 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Tue, 13 Sep 2016 15:58:20 -0700 Subject: [PATCH 14/18] 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. --- omaha/trivial_server.go | 100 ++++++++++++++++++++++++++++++++ omaha/trivial_server_test.go | 108 +++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 omaha/trivial_server.go create mode 100644 omaha/trivial_server_test.go diff --git a/omaha/trivial_server.go b/omaha/trivial_server.go new file mode 100644 index 0000000..2d38792 --- /dev/null +++ b/omaha/trivial_server.go @@ -0,0 +1,100 @@ +// 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" +) + +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 + } + return &tu.Update, nil +} + +// 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 +} diff --git a/omaha/trivial_server_test.go b/omaha/trivial_server_test.go new file mode 100644 index 0000000..df80d02 --- /dev/null +++ b/omaha/trivial_server_test.go @@ -0,0 +1,108 @@ +// 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) + } + 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) + } + + 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)) + } +} From acd5c75d52bc74d5b48443c18051b3d7b4da9fec Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Fri, 21 Apr 2017 14:07:12 -0700 Subject: [PATCH 15/18] travis: test against go 1.7 and 1.8 --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a1bddc0..8617bf3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: go -go: 1.1 +sudo: false +go: + - 1.7.5 + - 1.8.1 script: - go test -v ./... From 4b95d8178b297bcf7f73058e0814401ff5063a7a Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Mon, 24 Apr 2017 12:04:41 -0700 Subject: [PATCH 16/18] omaha: update upstream doc URL --- omaha/protocol.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omaha/protocol.go b/omaha/protocol.go index b7b152e..63d9b2c 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -19,7 +19,7 @@ // by the server to provide update information, if any, or to simply // acknowledge the receipt of event status. // -// https://github.com/google/omaha/blob/wiki/ServerProtocol.md +// https://github.com/google/omaha/blob/master/doc/ServerProtocolV3.md package omaha import ( From c7d81825c4e499b8917dcaa2477389cda342e013 Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Mon, 24 Apr 2017 12:07:47 -0700 Subject: [PATCH 17/18] omaha: remove lingering references to mantle --- omaha/protocol.go | 6 ++---- omaha/protocol_test.go | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/omaha/protocol.go b/omaha/protocol.go index 63d9b2c..00dbda3 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -24,8 +24,6 @@ package omaha import ( "encoding/xml" - - "github.com/coreos/mantle/version" ) // Request sent by the Omaha client @@ -49,7 +47,7 @@ type Request struct { func NewRequest() *Request { return &Request{ Protocol: "3.0", - Version: version.Version, + // TODO(marineam) set a default client Version OS: &OS{ Platform: LocalPlatform(), Arch: LocalArch(), @@ -135,7 +133,7 @@ type Response struct { func NewResponse() *Response { return &Response{ Protocol: "3.0", - Server: "mantle", + Server: "go-omaha", DayStart: DayStart{ElapsedSeconds: "0"}, } } diff --git a/omaha/protocol_test.go b/omaha/protocol_test.go index 54b6b0f..1980cf4 100644 --- a/omaha/protocol_test.go +++ b/omaha/protocol_test.go @@ -108,7 +108,7 @@ func ExampleNewResponse() { // Output: // - // + // // // // From 9a796427d5366c2b4bbff00a0c29809a25ef424c Mon Sep 17 00:00:00 2001 From: Michael Marineau Date: Mon, 24 Apr 2017 13:05:53 -0700 Subject: [PATCH 18/18] 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. --- omaha/protocol.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/omaha/protocol.go b/omaha/protocol.go index 00dbda3..b3ec12d 100644 --- a/omaha/protocol.go +++ b/omaha/protocol.go @@ -108,9 +108,9 @@ type UpdateRequest struct { } type PingRequest struct { - Active int `xml:"active,attr,omitempty"` - LastActiveReportDays int `xml:"a,attr,omitempty"` - LastReportDays int `xml:"r,attr,omitempty"` + Active int `xml:"active,attr,omitempty"` + LastActiveReportDays *int `xml:"a,attr,omitempty"` + LastReportDays int `xml:"r,attr,omitempty"` } type EventRequest struct {