Merge pull request #18 from marineam/from-mantle

Import omaha updates from mantle
This commit is contained in:
Michael Marineau 2017-05-02 12:34:24 -07:00 committed by GitHub
commit c694d6ed59
18 changed files with 1662 additions and 402 deletions

View file

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

181
omaha/codes.go Normal file
View file

@ -0,0 +1,181 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"fmt"
)
type EventType int
const (
EventTypeUnknown EventType = 0
EventTypeDownloadComplete EventType = 1
EventTypeInstallComplete EventType = 2
EventTypeUpdateComplete EventType = 3
EventTypeUninstall EventType = 4
EventTypeDownloadStarted EventType = 5
EventTypeInstallStarted EventType = 6
EventTypeNewApplicationInstallStarted EventType = 9
EventTypeSetupStarted EventType = 10
EventTypeSetupFinished EventType = 11
EventTypeUpdateApplicationStarted EventType = 12
EventTypeUpdateDownloadStarted EventType = 13
EventTypeUpdateDownloadFinished EventType = 14
EventTypeUpdateInstallerStarted EventType = 15
EventTypeSetupUpdateBegin EventType = 16
EventTypeSetupUpdateComplete EventType = 17
EventTypeRegisterProductComplete EventType = 20
EventTypeOEMInstallFirstCheck EventType = 30
EventTypeAppSpecificCommandStarted EventType = 40
EventTypeAppSpecificCommandEnded EventType = 41
EventTypeSetupFailure EventType = 100
EventTypeComServerFailure EventType = 102
EventTypeSetupUpdateFailure EventType = 103
)
func (e EventType) String() string {
switch e {
case EventTypeUnknown:
return "unknown"
case EventTypeDownloadComplete:
return "download complete"
case EventTypeInstallComplete:
return "install complete"
case EventTypeUpdateComplete:
return "update complete"
case EventTypeUninstall:
return "uninstall"
case EventTypeDownloadStarted:
return "download started"
case EventTypeInstallStarted:
return "install started"
case EventTypeNewApplicationInstallStarted:
return "new application install started"
case EventTypeSetupStarted:
return "setup started"
case EventTypeSetupFinished:
return "setup finished"
case EventTypeUpdateApplicationStarted:
return "update application started"
case EventTypeUpdateDownloadStarted:
return "update download started"
case EventTypeUpdateDownloadFinished:
return "update download finished"
case EventTypeUpdateInstallerStarted:
return "update installer started"
case EventTypeSetupUpdateBegin:
return "setup update begin"
case EventTypeSetupUpdateComplete:
return "setup update complete"
case EventTypeRegisterProductComplete:
return "register product complete"
case EventTypeOEMInstallFirstCheck:
return "OEM install first check"
case EventTypeAppSpecificCommandStarted:
return "app-specific command started"
case EventTypeAppSpecificCommandEnded:
return "app-specific command ended"
case EventTypeSetupFailure:
return "setup failure"
case EventTypeComServerFailure:
return "COM server failure"
case EventTypeSetupUpdateFailure:
return "setup update failure "
default:
return fmt.Sprintf("event %d", e)
}
}
type EventResult int
const (
EventResultError EventResult = 0
EventResultSuccess EventResult = 1
EventResultSuccessReboot EventResult = 2
EventResultSuccessRestartBrowser EventResult = 3
EventResultCancelled EventResult = 4
EventResultErrorInstallerMSI EventResult = 5
EventResultErrorInstallerOther EventResult = 6
EventResultNoUpdate EventResult = 7
EventResultInstallerSystem EventResult = 8
EventResultUpdateDeferred EventResult = 9
EventResultHandoffError EventResult = 10
)
func (e EventResult) String() string {
switch e {
case EventResultError:
return "error"
case EventResultSuccess:
return "success"
case EventResultSuccessReboot:
return "success reboot"
case EventResultSuccessRestartBrowser:
return "success restart browser"
case EventResultCancelled:
return "cancelled"
case EventResultErrorInstallerMSI:
return "error installer MSI"
case EventResultErrorInstallerOther:
return "error installer other"
case EventResultNoUpdate:
return "noupdate"
case EventResultInstallerSystem:
return "error installer system"
case EventResultUpdateDeferred:
return "update deferred"
case EventResultHandoffError:
return "handoff error"
default:
return fmt.Sprintf("result %d", e)
}
}
type AppStatus string
const (
// Standard values
AppOK AppStatus = "ok"
AppRestricted AppStatus = "restricted"
AppUnknownId AppStatus = "error-unknownApplication"
AppInvalidId AppStatus = "error-invalidAppId"
// Extra error values
AppInvalidVersion AppStatus = "error-invalidVersion"
AppInternalError AppStatus = "error-internal"
)
// Make AppStatus easy to use as an error
func (a AppStatus) Error() string {
return 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)
}

134
omaha/handler.go Normal file
View file

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

68
omaha/handler_test.go Normal file
View file

@ -0,0 +1,68 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"encoding/xml"
"fmt"
"testing"
"github.com/kylelemons/godebug/diff"
)
const (
testAppId = "{27BD862E-8AE8-4886-A055-F7F1A6460627}"
testAppVer = "1.0.0"
)
var (
nilRequest *Request
nilResponse *Response
)
func init() {
nilRequest = NewRequest()
nilRequest.AddApp(testAppId, testAppVer)
nilResponse = NewResponse()
nilResponse.AddApp(testAppId, AppOK)
}
func compareXML(a, b interface{}) error {
aXml, err := xml.MarshalIndent(a, "", "\t")
if err != nil {
return err
}
bXml, err := xml.MarshalIndent(b, "", "\t")
if err != nil {
return err
}
if d := diff.Diff(string(aXml), string(bXml)); d != "" {
err := fmt.Errorf("Unexpected XML:\n%s", d)
return err
}
return nil
}
func TestHandleNilRequest(t *testing.T) {
handler := OmahaHandler{UpdaterStub{}}
response := NewResponse()
handler.serveApp(response, nil, nilRequest, nilRequest.Apps[0])
if err := compareXML(nilResponse, response); err != nil {
t.Error(err)
}
}

32
omaha/isclosed.go Normal file
View file

@ -0,0 +1,32 @@
// Copyright 2016 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"net"
)
// isClosed detects if an error is due to a closed network connection,
// working around bug https://github.com/golang/go/issues/4373
func isClosed(err error) bool {
if err == nil {
return false
}
if operr, ok := err.(*net.OpError); ok {
err = operr.Err
}
// cry softly
return err.Error() == "use of closed network connection"
}

View file

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

View file

@ -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:
// <?xml version="1.0" encoding="UTF-8"?>
// <response protocol="3.0" server="unit-test">
// <daystart elapsed_seconds="0"></daystart>
// <app appid="{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}" status="ok">
// <ping status="ok"></ping>
// <updatecheck status="ok">
// <urls>
// <url codebase="http://localhost/updates"></url>
// </urls>
// <manifest version="9999.0.0">
// <packages>
// <package hash="+LXvjiaPkeYDLHoNKlf9qbJwvnk=" name="update.gz" size="67546213" required="true"></package>
// </packages>
// <actions>
// <action event="postinstall" ChromeOSVersion="9999.0.0" sha256="0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=" needsadmin="false" IsDelta="true" DisablePayloadBackoff="true"></action>
// </actions>
// </manifest>
// </updatecheck>
// </app>
// </response>
}
func ExampleOmaha_NewRequest() {
request := NewRequest("Indy", "Chrome OS", "ForcedUpdate_x86_64", "")
app := request.AddApp("{27BD862E-8AE8-4886-A055-F7F1A6460627}", "1.0.0.0")
app.AddUpdateCheck()
event := app.AddEvent()
event.Type = "1"
event.Result = "0"
if raw, err := xml.MarshalIndent(request, "", " "); err != nil {
fmt.Println(err)
return
} else {
fmt.Printf("%s%s\n", xml.Header, raw)
}
// Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <request protocol="3.0">
// <os platform="Chrome OS" version="Indy" sp="ForcedUpdate_x86_64"></os>
// <app appid="{27BD862E-8AE8-4886-A055-F7F1A6460627}" version="1.0.0.0">
// <updatecheck></updatecheck>
// <event eventtype="1" eventresult="0"></event>
// </app>
// </request>
}

112
omaha/package.go Normal file
View file

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

144
omaha/package_test.go Normal file
View file

@ -0,0 +1,144 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"strings"
"testing"
"github.com/kylelemons/godebug/pretty"
)
func TestPackageFromPath(t *testing.T) {
expect := Package{
Name: "null",
Sha1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
Sha256: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
Size: 0,
Required: false,
}
p := Package{}
if err := p.FromPath("/dev/null"); err != nil {
t.Fatal(err)
}
if diff := pretty.Compare(expect, p); diff != "" {
t.Errorf("Hashing /dev/null failed: %v", diff)
}
}
func TestProtocolFromReader(t *testing.T) {
data := strings.NewReader("testing\n")
expect := Package{
Name: "",
Sha1: "mAFznarkTsUpPU4fU9P00tQm2Rw=",
Sha256: "EqYfThc/s6EcBdZHH3Ryj3YjG0pfzZZnzvOvh6OuTcI=",
Size: 8,
Required: false,
}
p := Package{}
if err := p.FromReader(data); err != nil {
t.Fatal(err)
}
if diff := pretty.Compare(expect, p); diff != "" {
t.Errorf("Hashing failed: %v", diff)
}
}
func TestPackageVerify(t *testing.T) {
p := Package{
Name: "null",
Sha1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
Sha256: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
Size: 0,
Required: false,
}
if err := p.Verify("/dev"); err != nil {
t.Fatal(err)
}
}
func TestPackageVerifyNoSha256(t *testing.T) {
p := Package{
Name: "null",
Sha1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
Sha256: "",
Size: 0,
Required: false,
}
if err := p.Verify("/dev"); err != nil {
t.Fatal(err)
}
}
func TestPackageVerifyBadSize(t *testing.T) {
p := Package{
Name: "null",
Sha1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
Sha256: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
Size: 1,
Required: false,
}
err := p.Verify("/dev")
if err == nil {
t.Error("verify passed")
}
if err != PackageSizeMismatchError {
t.Error(err)
}
}
func TestPackageVerifyBadSha1(t *testing.T) {
p := Package{
Name: "null",
Sha1: "xxxxxxxxxxxxxxxxxxxxxxxxxxx=",
Sha256: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
Size: 0,
Required: false,
}
err := p.Verify("/dev")
if err == nil {
t.Error("verify passed")
}
if err != PackageHashMismatchError {
t.Error(err)
}
}
func TestPackageVerifyBadSha256(t *testing.T) {
p := Package{
Name: "null",
Sha1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
Sha256: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=",
Size: 0,
Required: false,
}
err := p.Verify("/dev")
if err == nil {
t.Error("verify passed")
}
if err != PackageHashMismatchError {
t.Error(err)
}
}

261
omaha/protocol.go Normal file
View file

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

163
omaha/protocol_test.go Normal file
View file

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="ChromeOSUpdateEngine-0.1.0.0" updaterversion="ChromeOSUpdateEngine-0.1.0.0" installsource="ondemandupdate" ismachine="1">
<os version="Indy" platform="Chrome OS" sp="ForcedUpdate_x86_64"></os>
<app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" bootid="{7D52A1CC-7066-40F0-91C7-7CB6A871BFDE}" machineid="{8BDE4C4D-9083-4D61-B41C-3253212C0C37}" oem="ec3000" version="ForcedUpdate" track="dev-channel" from_track="developer-build" lang="en-US" board="amd64-generic" hardware_class="" delta_okay="false" >
<ping active="1" a="-1" r="-1"></ping>
<updatecheck targetversionprefix=""></updatecheck>
<event eventtype="3" eventresult="2" previousversion=""></event>
</app>
</request>
`
func 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:
// <?xml version="1.0" encoding="UTF-8"?>
// <response protocol="3.0" server="go-omaha">
// <daystart elapsed_seconds="0"></daystart>
// <app appid="{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}" status="ok">
// <ping status="ok"></ping>
// <updatecheck status="ok">
// <urls>
// <url codebase="http://localhost/updates"></url>
// </urls>
// <manifest version="9999.0.0">
// <packages>
// <package name="update.gz" hash="+LXvjiaPkeYDLHoNKlf9qbJwvnk=" size="67546213" required="true"></package>
// </packages>
// <actions>
// <action event="postinstall" DisplayVersion="9999.0.0" sha256="0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=" IsDeltaPayload="true" DisablePayloadBackoff="true"></action>
// </actions>
// </manifest>
// </updatecheck>
// </app>
// </response>
}
func ExampleNewRequest() {
request := NewRequest()
request.Version = ""
request.OS = &OS{
Platform: "Chrome OS",
Version: "Indy",
ServicePack: "ForcedUpdate_x86_64",
}
app := request.AddApp("{27BD862E-8AE8-4886-A055-F7F1A6460627}", "1.0.0.0")
app.AddUpdateCheck()
event := app.AddEvent()
event.Type = EventTypeDownloadComplete
event.Result = EventResultError
if raw, err := xml.MarshalIndent(request, "", " "); err != nil {
fmt.Println(err)
return
} else {
fmt.Printf("%s%s\n", xml.Header, raw)
}
// Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <request protocol="3.0">
// <os platform="Chrome OS" version="Indy" sp="ForcedUpdate_x86_64"></os>
// <app appid="{27BD862E-8AE8-4886-A055-F7F1A6460627}" version="1.0.0.0">
// <updatecheck></updatecheck>
// <event eventtype="1" eventresult="0"></event>
// </app>
// </request>
}

73
omaha/server.go Normal file
View file

@ -0,0 +1,73 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"net"
"net/http"
)
func NewServer(addr string, updater Updater) (*Server, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
mux := http.NewServeMux()
srv := &http.Server{
Addr: addr,
Handler: mux,
}
s := &Server{
Updater: updater,
Mux: mux,
l: l,
srv: srv,
}
h := &OmahaHandler{s}
mux.Handle("/v1/update", h)
mux.Handle("/v1/update/", h)
return s, nil
}
type Server struct {
Updater
Mux *http.ServeMux
l net.Listener
srv *http.Server
}
func (s *Server) Serve() error {
err := s.srv.Serve(s.l)
if isClosed(err) {
// gracefully quit
err = nil
}
return nil
}
func (s *Server) Destroy() error {
return s.l.Close()
}
func (s *Server) Addr() net.Addr {
return s.l.Addr()
}

114
omaha/server_test.go Normal file
View file

@ -0,0 +1,114 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"bytes"
"encoding/xml"
"fmt"
"net/http"
"sync"
"testing"
"time"
)
type mockServer struct {
UpdaterStub
reqChan chan *Request
}
func (m *mockServer) CheckApp(req *Request, app *AppRequest) error {
m.reqChan <- req
return nil
}
func TestServerRequestResponse(t *testing.T) {
var wg sync.WaitGroup
defer wg.Wait()
// make an omaha server
svc := &mockServer{
reqChan: make(chan *Request),
}
s, err := NewServer("127.0.0.1:0", svc)
if err != nil {
t.Fatalf("failed to create omaha server: %v", err)
}
defer func() {
err := s.Destroy()
if err != nil {
t.Error(err)
}
close(svc.reqChan)
}()
wg.Add(1)
go func() {
defer wg.Done()
if err := s.Serve(); err != nil {
t.Errorf("Serve failed: %v", err)
}
}()
buf := new(bytes.Buffer)
enc := xml.NewEncoder(buf)
enc.Indent("", "\t")
err = enc.Encode(nilRequest)
if err != nil {
t.Fatalf("failed to marshal request: %v", err)
}
// check that server gets the same thing we sent
wg.Add(1)
go func() {
defer wg.Done()
sreq, ok := <-svc.reqChan
if !ok {
t.Errorf("failed to get notification from server")
return
}
if err := compareXML(nilRequest, sreq); err != nil {
t.Error(err)
}
}()
// send omaha request
endpoint := fmt.Sprintf("http://%s/v1/update/", s.Addr())
httpClient := &http.Client{
Timeout: 2 * time.Second,
}
res, err := httpClient.Post(endpoint, "text/xml", buf)
if err != nil {
t.Fatalf("failed to post: %v", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
t.Fatalf("failed to post: %v", res.Status)
}
dec := xml.NewDecoder(res.Body)
sresp := &Response{}
if err := dec.Decode(sresp); err != nil {
t.Fatalf("failed to parse body: %v", err)
}
if err := compareXML(nilResponse, sresp); err != nil {
t.Error(err)
}
}

53
omaha/system.go Normal file
View file

@ -0,0 +1,53 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"runtime"
)
// Translate GOARCH to Omaha's choice of names, because no two independent
// software projects *ever* use the same set of architecture names. ;-)
func LocalArch() string {
switch runtime.GOARCH {
case "386":
return "x86"
case "amd64":
return "x64"
case "amd64p32":
// Not actually specified by Omaha but it follows the above.
return "x32"
case "arm":
fallthrough
default:
// Nothing else is defined by Omaha so anything goes.
return runtime.GOARCH
}
}
// Translate GOOS to Omaha's platform names as best as we can.
func LocalPlatform() string {
switch runtime.GOOS {
case "darwin":
return "mac" // or "ios"
case "linux":
return "linux" // or "android"
case "windows":
return "win"
default:
// Nothing else is defined by Omaha so anything goes.
return runtime.GOOS
}
}

100
omaha/trivial_server.go Normal file
View file

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

View file

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

73
omaha/update.go Normal file
View file

@ -0,0 +1,73 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"encoding/xml"
)
// Update is a manifest for a single omaha update response. It extends
// the standard Manifest protocol element with the application id and
// previous version which are used to match against the update request.
// A blank previous version indicates this update can be applied to any
// existing install. The application id may not be blank.
type Update struct {
XMLName xml.Name `xml:"update" json:"-"`
Id string `xml:"appid,attr"`
PreviousVersion string `xml:"previousversion,attr,omitempty"`
URL URL `xml:"urls>url"`
Manifest
// The delta_okay request attribute is an update_engine extension.
RespectDeltaOK bool `xml:"respect_delta_okay,attr,omitempty"`
}
// The URL attribute in Update is currently assumed to be a relative
// path which may be found on multiple mirrors. A server using this is
// expected to know the mirror prefix(s) it can give the client.
func (u *Update) URLs(prefixes []string) []*URL {
urls := make([]*URL, len(prefixes))
for i, prefix := range prefixes {
urls[i] = &URL{CodeBase: prefix + u.URL.CodeBase}
}
return urls
}
// Updater provides a common interface for any backend that can respond to
// update requests made to an Omaha server.
type Updater interface {
CheckApp(req *Request, app *AppRequest) error
CheckUpdate(req *Request, app *AppRequest) (*Update, error)
Event(req *Request, app *AppRequest, event *EventRequest)
Ping(req *Request, app *AppRequest)
}
type UpdaterStub struct{}
func (u UpdaterStub) CheckApp(req *Request, app *AppRequest) error {
return nil
}
func (u UpdaterStub) CheckUpdate(req *Request, app *AppRequest) (*Update, error) {
return nil, NoUpdate
}
func (u UpdaterStub) Event(req *Request, app *AppRequest, event *EventRequest) {
return
}
func (u UpdaterStub) Ping(req *Request, app *AppRequest) {
return
}

42
omaha/update_test.go Normal file
View file

@ -0,0 +1,42 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package omaha
import (
"encoding/xml"
"testing"
)
const SampleUpdate = `<?xml version="1.0" encoding="UTF-8"?>
<update appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" version="9999.0.0">
<urls>
<url codebase="packages/9999.0.0"></url>
</urls>
<manifest version="9999.0.0">
<packages>
<package name="update.gz" hash="+LXvjiaPkeYDLHoNKlf9qbJwvnk=" size="67546213" required="true"></package>
</packages>
</update>
`
func TestUpdateURLs(t *testing.T) {
u := Update{}
xml.Unmarshal([]byte(SampleUpdate), &u)
urls := u.URLs([]string{"http://localhost/updates/"})
if urls[0].CodeBase != "http://localhost/updates/packages/9999.0.0" {
t.Error("Unexpected URL", urls[0].CodeBase)
}
}