Merge pull request #24 from marineam/client

Begin generic omaha client implementation
This commit is contained in:
Michael Marineau 2017-05-26 13:38:09 -07:00 committed by GitHub
commit f8acb2d7b7
10 changed files with 997 additions and 8 deletions

View File

@ -1,11 +1,24 @@
# Go Omaha
Implementation of the omaha protocol in Go.
[![Build Status](https://travis-ci.org/coreos/go-omaha.svg?branch=master)](https://travis-ci.org/coreos/go-omaha)
[![GoDoc](https://godoc.org/github.com/coreos/go-omaha/omaha?status.svg)](https://godoc.org/github.com/coreos/go-omaha/omaha)
https://github.com/google/omaha
Implementation of the [omaha update protocol](https://github.com/google/omaha) in Go.
## Docs
## Status
http://godoc.org/github.com/coreos/go-omaha/omaha
This code is targeted for use with CoreOS's [CoreUpdate](https://coreos.com/products/coreupdate/) product and the Container Linux [update_engine](https://github.com/coreos/update_engine).
As a result this is not a complete implementation of the [protocol](https://github.com/google/omaha/blob/master/doc/ServerProtocolV3.md) and inherits a number of quirks from update_engine.
These differences include:
[![Build Status](https://travis-ci.org/coreos/go-omaha.png)](https://travis-ci.org/coreos/go-omaha)
- No offline activity tracking.
The protocol's ping mechanism allows for tracking application usage, reporting the number of days since the last ping and how many of those days saw active usage.
CoreUpdate does not use this, instead assuming update clients are always online and checking in once every ~45-50 minutes.
Each check in should include a ping and optionally an update check.
- Various protocol extensions/abuses.
update_engine, likely due to earlier limitations of the protocol and Google's server implementation, uses a number of non-standard fields.
For example, packing a lot of extra attributes such as the package's SHA-256 hash into a "postinstall" action.
As much as possible the code includes comments about these extensions.
- Many data fields not used by CoreUpdate are omitted.

274
omaha/client/client.go Normal file
View File

@ -0,0 +1,274 @@
// Copyright 2017 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 client provides a general purpose Omaha update client implementation.
package client
import (
"errors"
"fmt"
"net/url"
"github.com/satori/go.uuid"
"github.com/coreos/go-omaha/omaha"
)
// Client supports managing multiple apps using a single server.
type Client struct {
apiClient *httpClient
apiEndpoint string
clientVersion string
userID string
sessionID string
isMachine bool
apps map[string]*AppClient
}
// AppClient supports managing a single application.
type AppClient struct {
*Client
appID string
track string
version string
}
// New creates an omaha client for updating one or more applications.
// userID must be a persistent unique identifier of this update client.
func New(serverURL, userID string) (*Client, error) {
if userID == "" {
return nil, errors.New("omaha: empty user identifier")
}
c := &Client{
apiClient: newHTTPClient(),
clientVersion: "go-omaha",
userID: userID,
sessionID: uuid.NewV4().String(),
apps: make(map[string]*AppClient),
}
if err := c.SetServerURL(serverURL); err != nil {
return nil, err
}
return c, nil
}
// SetServerURL changes the Omaha server this client talks to.
// If the URL does not include a path component /v1/update/ is assumed.
func (c *Client) SetServerURL(serverURL string) error {
u, err := url.Parse(serverURL)
if err != nil {
return fmt.Errorf("omaha: invalid server URL: %v", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("omaha: invalid server protocol: %s", u)
}
if u.Host == "" {
return fmt.Errorf("omaha: invalid server host: %s", u)
}
if u.Path == "" || u.Path == "/" {
u.Path = "/v1/update/"
}
c.apiEndpoint = u.String()
return nil
}
// SetClientVersion sets the identifier of this updater application.
// e.g. "update_engine-0.1.0". Default is "go-omaha".
func (c *Client) SetClientVersion(clientVersion string) {
c.clientVersion = clientVersion
}
// AppClient gets the application client for the given application ID.
func (c *Client) AppClient(appID string) (*AppClient, error) {
if app, ok := c.apps[appID]; ok {
return app, nil
}
return nil, fmt.Errorf("omaha: missing app client %q", appID)
}
// NewAppClient creates a new application client.
func (c *Client) NewAppClient(appID, appVersion string) (*AppClient, error) {
if _, ok := c.apps[appID]; ok {
return nil, fmt.Errorf("omaha: duplicate app client %q", appID)
}
ac := &AppClient{
Client: c,
appID: appID,
}
c.apps[appID] = ac
return ac, nil
}
// NewAppClient creates a single application client.
// Shorthand for New(serverURL, userID).NewAppClient(appID, appVersion).
func NewAppClient(serverURL, userID, appID, appVersion string) (*AppClient, error) {
c, err := New(serverURL, userID)
if err != nil {
return nil, err
}
ac, err := c.NewAppClient(appID, appVersion)
if err := ac.SetVersion(appVersion); err != nil {
return nil, err
}
return ac, nil
}
// SetVersion changes the application version.
func (ac *AppClient) SetVersion(version string) error {
if version == "" {
return errors.New("omaha: empty application version")
}
ac.version = version
return nil
}
// SetTrack sets the application update track or group.
// This is a update_engine/Core Update protocol extension.
func (ac *AppClient) SetTrack(track string) error {
// Although track is an omaha extension and theoretically not required
// our Core Update server requires track to be set to a valid id/name.
// TODO: deprecate track and use the standard cohort protocol fields.
if track == "" {
return errors.New("omaha: empty application update track/group")
}
ac.track = track
return nil
}
func (ac *AppClient) UpdateCheck() (*omaha.UpdateResponse, error) {
req := ac.newReq()
app := req.Apps[0]
app.AddPing()
app.AddUpdateCheck()
appResp, err := ac.doReq(ac.apiEndpoint, req)
if err != nil {
return nil, err
}
if appResp.Ping == nil {
return nil, fmt.Errorf("omaha: ping status missing from response")
}
if appResp.Ping.Status != "ok" {
return nil, fmt.Errorf("omaha: ping status %s", appResp.Ping.Status)
}
if appResp.UpdateCheck == nil {
return nil, fmt.Errorf("omaha: update check missing from response")
}
if appResp.UpdateCheck.Status != omaha.UpdateOK {
return nil, appResp.UpdateCheck.Status
}
return appResp.UpdateCheck, nil
}
func (ac *AppClient) Ping() error {
req := ac.newReq()
app := req.Apps[0]
app.AddPing()
appResp, err := ac.doReq(ac.apiEndpoint, req)
if err != nil {
return err
}
if appResp.Ping == nil {
return fmt.Errorf("omaha: ping status missing from response")
}
if appResp.Ping.Status != "ok" {
return fmt.Errorf("omaha: ping status %s", appResp.Ping.Status)
}
return nil
}
func (ac *AppClient) Event(event *omaha.EventRequest) error {
req := ac.newReq()
app := req.Apps[0]
app.Events = append(app.Events, event)
appResp, err := ac.doReq(ac.apiEndpoint, req)
if err != nil {
return err
}
if len(appResp.Events) == 0 {
return fmt.Errorf("omaha: event status missing from response")
}
if appResp.Events[0].Status != "ok" {
return fmt.Errorf("omaha: event status %s", appResp.Events[0].Status)
}
return nil
}
func (ac *AppClient) newReq() *omaha.Request {
req := omaha.NewRequest()
req.Version = ac.clientVersion
req.UserID = ac.userID
req.SessionID = ac.sessionID
if ac.isMachine {
req.IsMachine = 1
}
app := req.AddApp(ac.appID, ac.version)
app.Track = ac.track
// MachineID and BootID are non-standard fields used by CoreOS'
// update_engine and Core Update. Copy their values from the
// standard UserID and SessionID. Eventually the non-standard
// fields should be deprecated.
app.MachineID = req.UserID
app.BootID = req.SessionID
return req
}
func (ac *AppClient) doReq(url string, req *omaha.Request) (*omaha.AppResponse, error) {
if len(req.Apps) != 1 {
panic(fmt.Errorf("unexpected number of apps: %d", len(req.Apps)))
}
appID := req.Apps[0].ID
resp, err := ac.apiClient.Omaha(url, req)
if err != nil {
return nil, err
}
appResp := resp.GetApp(appID)
if appResp == nil {
return nil, fmt.Errorf("omaha: app %s missing from response", appID)
}
if appResp.Status != omaha.AppOK {
return nil, appResp.Status
}
return appResp, nil
}

181
omaha/client/client_test.go Normal file
View File

@ -0,0 +1,181 @@
// Copyright 2017 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 client
import (
"reflect"
"testing"
"github.com/coreos/go-omaha/omaha"
)
// implements omaha.Updater
type recorder struct {
t *testing.T
update *omaha.Update
checks []*omaha.UpdateRequest
events []*omaha.EventRequest
pings []*omaha.PingRequest
}
func newRecordingServer(t *testing.T, u *omaha.Update) (*recorder, *omaha.Server) {
r := &recorder{t: t, update: u}
s, err := omaha.NewServer("127.0.0.1:0", r)
if err != nil {
t.Fatal(err)
}
go s.Serve()
return r, s
}
func (r *recorder) CheckApp(req *omaha.Request, app *omaha.AppRequest) error {
// CheckApp is meant for checking if app.ID is valid but we don't
// care and accept any ID. Instead this is just a convenient place
// to check that all requests are well formed.
if len(req.SessionID) != 36 {
r.t.Errorf("SessionID %q is not a UUID", req.SessionID)
}
if app.BootID != req.SessionID {
r.t.Errorf("BootID %q != SessionID %q", app.BootID, req.SessionID)
}
if req.UserID == "" {
r.t.Error("UserID is blank")
}
if app.MachineID != req.UserID {
r.t.Errorf("MachineID %q != UserID %q", app.MachineID, req.UserID)
}
if app.Version == "" {
r.t.Error("App Version is blank")
}
return nil
}
func (r *recorder) CheckUpdate(req *omaha.Request, app *omaha.AppRequest) (*omaha.Update, error) {
r.checks = append(r.checks, app.UpdateCheck)
if r.update == nil {
return nil, omaha.NoUpdate
} else {
return r.update, nil
}
}
func (r *recorder) Event(req *omaha.Request, app *omaha.AppRequest, event *omaha.EventRequest) {
r.events = append(r.events, event)
}
func (r *recorder) Ping(req *omaha.Request, app *omaha.AppRequest) {
r.pings = append(r.pings, app.Ping)
}
func TestClientNoUpdate(t *testing.T) {
r, s := newRecordingServer(t, nil)
defer s.Destroy()
url := "http://" + s.Addr().String()
ac, err := NewAppClient(url, "client-id", "app-id", "0.0.0")
if err != nil {
t.Fatal(err)
}
if _, err := ac.UpdateCheck(); err != omaha.NoUpdate {
t.Fatalf("UpdateCheck id not return NoUpdate: %v", err)
}
if len(r.pings) != 1 {
t.Fatalf("expected 1 ping, not %d", len(r.pings))
}
if len(r.checks) != 1 {
t.Fatalf("expected 1 update check, not %d", len(r.checks))
}
}
func TestClientWithUpdate(t *testing.T) {
r, s := newRecordingServer(t, &omaha.Update{
Manifest: omaha.Manifest{
Version: "1.1.1",
},
})
defer s.Destroy()
url := "http://" + s.Addr().String()
ac, err := NewAppClient(url, "client-id", "app-id", "0.0.0")
if err != nil {
t.Fatal(err)
}
update, err := ac.UpdateCheck()
if err != nil {
t.Fatal(err)
}
if update.Manifest.Version != "1.1.1" {
t.Fatalf("expected version 1.1.1, not %s", update.Manifest.Version)
}
if len(r.pings) != 1 {
t.Fatalf("expected 1 ping, not %d", len(r.pings))
}
if len(r.checks) != 1 {
t.Fatalf("expected 1 update check, not %d", len(r.checks))
}
}
func TestClientPing(t *testing.T) {
r, s := newRecordingServer(t, nil)
defer s.Destroy()
url := "http://" + s.Addr().String()
ac, err := NewAppClient(url, "client-id", "app-id", "0.0.0")
if err != nil {
t.Fatal(err)
}
if err := ac.Ping(); err != nil {
t.Fatal(err)
}
if len(r.pings) != 1 {
t.Fatalf("expected 1 ping, not %d", len(r.pings))
}
}
func TestClientEvent(t *testing.T) {
r, s := newRecordingServer(t, nil)
defer s.Destroy()
url := "http://" + s.Addr().String()
ac, err := NewAppClient(url, "client-id", "app-id", "0.0.0")
if err != nil {
t.Fatal(err)
}
event := &omaha.EventRequest{
Type: omaha.EventTypeDownloadComplete,
Result: omaha.EventResultSuccess,
}
if err := ac.Event(event); err != nil {
t.Fatal(err)
}
if len(r.events) != 1 {
t.Fatalf("expected 1 event, not %d", len(r.events))
}
if !reflect.DeepEqual(event, r.events[0]) {
t.Fatalf("sent != received:\n%#v\n%#v", event, r.events[0])
}
}

73
omaha/client/error.go Normal file
View File

@ -0,0 +1,73 @@
// Copyright 2017 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 client
import (
"encoding/xml"
"errors"
"io"
"net/http"
)
var (
bodySizeError = errors.New("http response exceeded 1MB")
bodyEmptyError = errors.New("http response was empty")
)
// xml doesn't return the standard io.ErrUnexpectedEOF so check for both.
func isUnexpectedEOF(err error) bool {
if xerr, ok := err.(*xml.SyntaxError); ok {
return xerr.Msg == "unexpected EOF"
}
return err == io.ErrUnexpectedEOF
}
// httpError implements error and net.Error for http responses.
type httpError struct {
*http.Response
}
func (he *httpError) Error() string {
return "http error: " + he.Status
}
func (he *httpError) Timeout() bool {
switch he.StatusCode {
case http.StatusRequestTimeout: // 408
return true
case http.StatusGatewayTimeout: // 504
return true
default:
return false
}
}
func (he *httpError) Temporary() bool {
if he.Timeout() {
return true
}
switch he.StatusCode {
case http.StatusTooManyRequests: // 429
return true
case http.StatusInternalServerError: // 500
return true
case http.StatusBadGateway: // 502
return true
case http.StatusServiceUnavailable: // 503
return true
default:
return false
}
}

95
omaha/client/http.go Normal file
View File

@ -0,0 +1,95 @@
// Copyright 2017 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 client
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/coreos/go-omaha/omaha"
)
const (
defaultTimeout = 90 * time.Second
defaultTries = 7
)
// httpClient extends the standard http.Client to support xml encoding
// and decoding as well as automatic retries on transient failures.
type httpClient struct {
http.Client
}
func newHTTPClient() *httpClient {
return &httpClient{http.Client{
Timeout: defaultTimeout,
}}
}
// doPost sends a single HTTP POST, returning a parsed omaha response.
func (hc *httpClient) doPost(url string, reqBody []byte) (*omaha.Response, error) {
resp, err := hc.Post(url, "text/xml; charset=utf-8", bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
defer resp.Body.Close()
// A response over 1M in size is certainly bogus.
respBody := &io.LimitedReader{R: resp.Body, N: 1024 * 1024}
contentType := resp.Header.Get("Content-Type")
omahaResp, err := omaha.ParseResponse(contentType, respBody)
// Report a more sensible error if we truncated the body.
if isUnexpectedEOF(err) && respBody.N <= 0 {
err = bodySizeError
} else if err == io.EOF {
err = bodyEmptyError
}
// Prefer reporting HTTP errors over XML parsing errors.
if resp.StatusCode != http.StatusOK {
err = &httpError{resp}
}
return omahaResp, err
}
// Omaha encodes and sends an omaha request, retrying on any transient errors.
func (hc *httpClient) Omaha(url string, req *omaha.Request) (resp *omaha.Response, err error) {
buf := bytes.NewBufferString(xml.Header)
enc := xml.NewEncoder(buf)
if err := enc.Encode(req); err != nil {
return nil, fmt.Errorf("omaha: failed to encode request: %v", err)
}
for i := 0; i < defaultTries; i++ {
resp, err = hc.doPost(url, buf.Bytes())
if neterr, ok := err.(net.Error); ok && neterr.Temporary() {
// TODO(marineam): add exponential backoff
continue
}
break
}
if err != nil {
return nil, fmt.Errorf("omaha: request failed: %v", err)
}
return resp, nil
}

212
omaha/client/http_test.go Normal file
View File

@ -0,0 +1,212 @@
// Copyright 2017 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 client
import (
"bytes"
"net"
"net/http"
"strings"
"testing"
"github.com/coreos/go-omaha/omaha"
)
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 TestHTTPClientDoPost(t *testing.T) {
s, err := omaha.NewTrivialServer("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer s.Destroy()
go s.Serve()
c := newHTTPClient()
url := "http://" + s.Addr().String() + "/v1/update/"
resp, err := c.doPost(url, []byte(sampleRequest))
if err != nil {
t.Fatal(err)
}
if len(resp.Apps) != 1 {
t.Fatalf("Should be 1 app, not %d", len(resp.Apps))
}
if resp.Apps[0].Status != omaha.AppOK {
t.Fatalf("Bad apps status: %q", resp.Apps[0].Status)
}
}
type flakyHandler struct {
omaha.OmahaHandler
flakes int
reqs int
}
func (f *flakyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.reqs++
if f.flakes > 0 {
f.flakes--
http.Error(w, "Flake!", http.StatusInternalServerError)
return
}
f.OmahaHandler.ServeHTTP(w, r)
}
type flakyServer struct {
l net.Listener
s *http.Server
h *flakyHandler
}
func newFlakyServer() (*flakyServer, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
f := &flakyServer{
l: l,
s: &http.Server{},
h: &flakyHandler{
OmahaHandler: omaha.OmahaHandler{
Updater: omaha.UpdaterStub{},
},
flakes: 1,
},
}
f.s.Handler = f.h
go f.s.Serve(l)
return f, nil
}
func TestHTTPClientError(t *testing.T) {
f, err := newFlakyServer()
if err != nil {
t.Fatal(err)
}
defer f.l.Close()
c := newHTTPClient()
url := "http://" + f.l.Addr().String()
_, err = c.doPost(url, []byte(sampleRequest))
switch err := err.(type) {
case nil:
t.Fatal("doPost succeeded but should have failed")
case *httpError:
if err.StatusCode != http.StatusInternalServerError {
t.Fatalf("Unexpected http error: %v", err)
}
if err.Timeout() {
t.Fatal("http 500 error reported as timeout")
}
if !err.Temporary() {
t.Fatal("http 500 error not reported as temporary")
}
default:
t.Fatalf("Unexpected error: %v", err)
}
}
func TestHTTPClientRetry(t *testing.T) {
f, err := newFlakyServer()
if err != nil {
t.Fatal(err)
}
defer f.l.Close()
req, err := omaha.ParseRequest("", strings.NewReader(sampleRequest))
if err != nil {
t.Fatal(err)
}
c := newHTTPClient()
url := "http://" + f.l.Addr().String()
resp, err := c.Omaha(url, req)
if err != nil {
t.Fatal(err)
}
if len(resp.Apps) != 1 {
t.Fatalf("Should be 1 app, not %d", len(resp.Apps))
}
if resp.Apps[0].Status != omaha.AppOK {
t.Fatalf("Bad apps status: %q", resp.Apps[0].Status)
}
if f.h.reqs != 2 {
t.Fatalf("Server received %d requests, not 2", f.h.reqs)
}
}
// should result in an unexected EOF
func largeHandler1(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><response protocol="3.0">`))
w.Write(bytes.Repeat([]byte{' '}, 2*1024*1024))
w.Write([]byte(`</response>`))
}
// should result in an EOF
func largeHandler2(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
w.Write(bytes.Repeat([]byte{' '}, 2*1024*1024))
}
func TestHTTPClientLarge(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
s := &http.Server{
Handler: http.HandlerFunc(largeHandler1),
}
go s.Serve(l)
c := newHTTPClient()
url := "http://" + l.Addr().String()
_, err = c.doPost(url, []byte(sampleRequest))
if err != bodySizeError {
t.Errorf("Unexpected error: %v", err)
}
// switch to failing before XML is read instead of half-way
// through (which results in a different error internally)
s.Handler = http.HandlerFunc(largeHandler2)
_, err = c.doPost(url, []byte(sampleRequest))
if err != bodyEmptyError {
t.Errorf("Unexpected error: %v", err)
}
}

View File

@ -0,0 +1,75 @@
// Copyright 2017 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.
// +build linux
package client
import (
"bytes"
"fmt"
"io/ioutil"
)
const (
machineIDPath = "/etc/machine-id"
bootIDPath = "/proc/sys/kernel/random/boot_id"
)
// NewMachineClient creates a machine-wide client, updating applications
// that may be used by multiple users. On Linux the system's machine id
// is used as the user id, and boot id is used as the omaha session id.
func NewMachineClient(serverURL string) (*Client, error) {
machineID, err := ioutil.ReadFile(machineIDPath)
if err != nil {
fmt.Errorf("omaha: failed to read machine id: %v", err)
}
machineID = bytes.TrimSpace(machineID)
// Although machineID should be a UUID, it is formatted as a
// plain hex string, omitting the normal '-' separators, so it
// should be 32 bytes long. It would be nice to reformat it to
// add the '-' chars but update_engine doesn't so stick with its
// behavior for now.
if len(machineID) < 32 {
fmt.Errorf("omaha: incomplete machine id: %q",
machineID)
}
bootID, err := ioutil.ReadFile(bootIDPath)
if err != nil {
fmt.Errorf("omaha: failed to read boot id: %v", err)
}
bootID = bytes.TrimSpace(bootID)
// unlike machineID, bootID *does* include '-' chars.
if len(bootID) < 36 {
fmt.Errorf("omaha: incomplete boot id: %q", bootID)
}
c := &Client{
apiClient: newHTTPClient(),
clientVersion: "go-omaha",
userID: string(machineID),
sessionID: string(bootID),
isMachine: true,
apps: make(map[string]*AppClient),
}
if err := c.SetServerURL(serverURL); err != nil {
return nil, err
}
return c, nil
}

View File

@ -0,0 +1,48 @@
// Copyright 2017 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.
// +build linux
package client
import (
"bytes"
"io/ioutil"
"testing"
)
// skip test if external file isn't readable
func readOrSkip(t *testing.T, name string) string {
data, err := ioutil.ReadFile(name)
if err != nil {
t.Skip(err)
}
return string(bytes.TrimSpace(data))
}
func TestNewMachine(t *testing.T) {
userID := readOrSkip(t, machineIDPath)
sessionID := readOrSkip(t, bootIDPath)
c, err := NewMachineClient("https://example.com")
if err != nil {
t.Fatal(err)
}
if c.userID != userID {
t.Errorf("%q != %q", c.userID, userID)
}
if c.sessionID != sessionID {
t.Errorf("%q != %q", c.sessionID, sessionID)
}
}

View File

@ -160,7 +160,7 @@ const (
// Make AppStatus easy to use as an error
func (a AppStatus) Error() string {
return string(a)
return "omaha: app status " + string(a)
}
type UpdateStatus string
@ -177,5 +177,5 @@ const (
// Make UpdateStatus easy to use as an error
func (u UpdateStatus) Error() string {
return string(u)
return "omaha: update status " + string(u)
}

View File

@ -34,7 +34,7 @@ type Request struct {
Apps []*AppRequest `xml:"app"`
Protocol string `xml:"protocol,attr"`
InstallSource string `xml:"installsource,attr,omitempty"`
IsMachine string `xml:"ismachine,attr,omitempty"`
IsMachine int `xml:"ismachine,attr,omitempty"`
RequestID string `xml:"requestid,attr,omitempty"`
SessionID string `xml:"sessionid,attr,omitempty"`
TestSource string `xml:"testsource,attr,omitempty"`
@ -79,6 +79,15 @@ func (r *Request) AddApp(id, version string) *AppRequest {
return a
}
func (r *Request) GetApp(id string) *AppRequest {
for _, app := range r.Apps {
if app.ID == id {
return app
}
}
return nil
}
type AppRequest struct {
Ping *PingRequest `xml:"ping"`
UpdateCheck *UpdateRequest `xml:"updatecheck"`
@ -181,6 +190,15 @@ func (r *Response) AddApp(id string, status AppStatus) *AppResponse {
return a
}
func (r *Response) GetApp(id string) *AppResponse {
for _, app := range r.Apps {
if app.ID == id {
return app
}
}
return nil
}
type AppResponse struct {
Ping *PingResponse `xml:"ping"`
UpdateCheck *UpdateResponse `xml:"updatecheck"`