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