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 ./...
diff --git a/omaha/codes.go b/omaha/codes.go
new file mode 100644
index 0000000..783491a
--- /dev/null
+++ b/omaha/codes.go
@@ -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 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 string(u)
+}
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/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/omaha.go b/omaha/omaha.go
deleted file mode 100644
index 8e06aef..0000000
--- a/omaha/omaha.go
+++ /dev/null
@@ -1,264 +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.
- https://github.com/google/omaha/blob/wiki/ServerProtocol.md
-*/
-package omaha
-
-import (
- "encoding/xml"
-)
-
-type Request struct {
- XMLName xml.Name `xml:"request" datastore:"-"`
- 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" datastore:"-" json:"-"`
- 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" datastore"-" json:"-"`
- 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 string `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"`
-}
-
-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" datastore:"-" json:"-"`
- 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" datastore:"-" json:"-"`
- 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"`
-}
-
-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" 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 Urls struct {
- XMLName xml.Name `xml:"urls" datastore:"-" json:"-"`
- Urls []Url `xml:"url" json:",omitempty"`
-}
-
-type Url struct {
- XMLName xml.Name `xml:"url" datastore:"-" json:"-"`
- 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"`
-}
-
-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" datastore:"-" json:"-"`
- Actions []*Action `xml:"action" json:",omitempty"`
-}
-
-type Action struct {
- XMLName xml.Name `xml:"action" datastore:"-" json:"-"`
- 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"`
- DisablePayloadBackoff bool `xml:"DisablePayloadBackoff,attr,omitempty"`
- MetadataSignatureRsa string `xml:"MetadataSignatureRsa,attr,omitempty"`
- 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/omaha_test.go b/omaha/omaha_test.go
deleted file mode 100644
index 5a3eae4..0000000
--- a/omaha/omaha_test.go
+++ /dev/null
@@ -1,137 +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].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 MachineId")
- }
-
- 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 != "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
- 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:
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
-}
-
-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:
- //
- //
- //
- //
- //
- //
- //
- //
-}
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
new file mode 100644
index 0000000..b3ec12d
--- /dev/null
+++ b/omaha/protocol.go
@@ -0,0 +1,261 @@
+// 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"
+)
+
+// 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"`
+
+ // 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
+ },
+ }
+}
+
+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"`
+ 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"`
+ OEMVersion string `xml:"oemversion,attr,omitempty"`
+ AlephVersion string `xml:"alephversion,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"`
+ NextVersion string `xml:"nextversion,attr,omitempty"`
+ 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 []*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"},
+ }
+}
+
+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
+}
+
+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 Event struct {
+ 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 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"`
+}
diff --git a/omaha/protocol_test.go b/omaha/protocol_test.go
new file mode 100644
index 0000000..1980cf4
--- /dev/null
+++ b/omaha/protocol_test.go
@@ -0,0 +1,163 @@
+// 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"
+)
+
+const SampleRequest = `
+
+
+
+
+
+
+
+
+`
+
+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.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 MachineId")
+ }
+
+ 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 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:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+}
+
+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:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+}
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)
+ }
+}
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
+ }
+}
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))
+ }
+}
diff --git a/omaha/update.go b/omaha/update.go
new file mode 100644
index 0000000..ce2188e
--- /dev/null
+++ b/omaha/update.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 (
+ "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
+}
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)
+ }
+}