Merge pull request #18 from marineam/from-mantle
Import omaha updates from mantle
This commit is contained in:
commit
c694d6ed59
18 changed files with 1662 additions and 402 deletions
|
@ -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
181
omaha/codes.go
Normal 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
134
omaha/handler.go
Normal 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
68
omaha/handler_test.go
Normal 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
32
omaha/isclosed.go
Normal 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"
|
||||
}
|
264
omaha/omaha.go
264
omaha/omaha.go
|
@ -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",
|
||||
}
|
|
@ -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
112
omaha/package.go
Normal 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
144
omaha/package_test.go
Normal 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
261
omaha/protocol.go
Normal 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
163
omaha/protocol_test.go
Normal 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
73
omaha/server.go
Normal 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
114
omaha/server_test.go
Normal 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
53
omaha/system.go
Normal 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
100
omaha/trivial_server.go
Normal 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
|
||||
}
|
108
omaha/trivial_server_test.go
Normal file
108
omaha/trivial_server_test.go
Normal 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
73
omaha/update.go
Normal 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
42
omaha/update_test.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue