protocol: add ParseRequest and ParseResponse functions

For parsing and verification of of HTTP request and response bodies,
including optional checking the Content-Type field which the handler
previously didn't do.
This commit is contained in:
Michael Marineau 2017-05-04 13:00:46 -07:00
parent 6198ba9443
commit a6290c1b4f
5 changed files with 252 additions and 16 deletions

View file

@ -33,25 +33,18 @@ func (o *OmahaHandler) ServeHTTP(w http.ResponseWriter, httpReq *http.Request) {
// 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)
contentType := httpReq.Header.Get("Content-Type")
omahaReq, err := ParseRequest(contentType, reader)
if err != nil {
log.Printf("omaha: Failed parsing request: %v", err)
http.Error(w, "Bad Omaha Request", http.StatusBadRequest)
return
}
httpStatus := 0
omahaResp := NewResponse()
for _, appReq := range omahaReq.Apps {
appResp := o.serveApp(omahaResp, httpReq, &omahaReq, appReq)
appResp := o.serveApp(omahaResp, httpReq, omahaReq, appReq)
if appResp.Status == AppOK {
// HTTP is ok if any app is ok.
httpStatus = http.StatusOK

71
omaha/parse.go Normal file
View file

@ -0,0 +1,71 @@
// 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 omaha
import (
"encoding/xml"
"fmt"
"io"
"mime"
"strings"
)
// checkContentType verifies the HTTP Content-Type header properly
// declares the document is XML and UTF-8. Blank is assumed OK.
func checkContentType(contentType string) error {
if contentType == "" {
return nil
}
mType, mParams, err := mime.ParseMediaType(contentType)
if err != nil {
return err
}
if mType != "text/xml" && mType != "application/xml" {
return fmt.Errorf("unsupported content type %q", mType)
}
charset, _ := mParams["charset"]
if charset != "" && strings.ToLower(charset) != "utf-8" {
return fmt.Errorf("unsupported content charset %q", charset)
}
return nil
}
// parseReqOrResp parses Request and Response objects.
func parseReqOrResp(r io.Reader, v interface{}) error {
decoder := xml.NewDecoder(r)
if err := decoder.Decode(v); err != nil {
return err
}
var protocol string
switch v := v.(type) {
case *Request:
protocol = v.Protocol
case *Response:
protocol = v.Protocol
default:
panic(fmt.Errorf("unexpected type %T", v))
}
if protocol != "3.0" {
return fmt.Errorf("unsupported omaha protocol: %q", protocol)
}
return nil
}

55
omaha/parse_test.go Normal file
View file

@ -0,0 +1,55 @@
// 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 omaha
import (
"strings"
"testing"
)
func TestCheckContentType(t *testing.T) {
for _, tt := range []struct {
ct string
ok bool
}{
{"", true},
{"text/xml", true},
{"text/XML", true},
{"application/xml", true},
{"text/plain", false},
{"xml", false},
{"text/xml; charset=utf-8", true},
{"text/xml; charset=UTF-8", true},
{"text/xml; charset=ascii", false},
} {
err := checkContentType(tt.ct)
if tt.ok && err != nil {
t.Errorf("%q failed: %v", tt.ct, err)
}
if !tt.ok && err == nil {
t.Errorf("%q was not rejected", tt.ct)
}
}
}
func TestParseBadVersion(t *testing.T) {
r := strings.NewReader(`<request protocol="2.0"></request>`)
err := parseReqOrResp(r, &Request{})
if err == nil {
t.Error("Bad protocol version was accepted")
} else if err.Error() != `unsupported omaha protocol: "2.0"` {
t.Errorf("Wrong error: %v", err)
}
}

View file

@ -24,6 +24,7 @@ package omaha
import (
"encoding/xml"
"io"
)
// Request sent by the Omaha client
@ -56,6 +57,22 @@ func NewRequest() *Request {
}
}
// ParseRequest verifies and returns the parsed Request document.
// The MIME Content-Type header may be provided to sanity check its
// value; if blank it is assumed to be XML in UTF-8.
func ParseRequest(contentType string, body io.Reader) (*Request, error) {
if err := checkContentType(contentType); err != nil {
return nil, err
}
r := &Request{}
if err := parseReqOrResp(body, r); err != nil {
return nil, err
}
return r, nil
}
func (r *Request) AddApp(id, version string) *AppRequest {
a := &AppRequest{ID: id, Version: version}
r.Apps = append(r.Apps, a)
@ -138,6 +155,22 @@ func NewResponse() *Response {
}
}
// ParseResponse verifies and returns the parsed Response document.
// The MIME Content-Type header may be provided to sanity check its
// value; if blank it is assumed to be XML in UTF-8.
func ParseResponse(contentType string, body io.Reader) (*Response, error) {
if err := checkContentType(contentType); err != nil {
return nil, err
}
r := &Response{}
if err := parseReqOrResp(body, r); err != nil {
return nil, err
}
return r, nil
}
type DayStart struct {
ElapsedSeconds string `xml:"elapsed_seconds,attr"`
}

View file

@ -17,10 +17,13 @@ package omaha
import (
"encoding/xml"
"fmt"
"reflect"
"strings"
"testing"
)
const SampleRequest = `<?xml version="1.0" encoding="UTF-8"?>
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" >
@ -30,10 +33,34 @@ const SampleRequest = `<?xml version="1.0" encoding="UTF-8"?>
</app>
</request>
`
sampleResponse = `<?xml version="1.0" encoding="UTF-8"?>
<response protocol="3.0">
<daystart elapsed_seconds="49008"/>
<app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" status="ok">
<ping status="ok"/>
<updatecheck status="ok">
<urls>
<url codebase="http://kam:8080/static/"/>
</urls>
<manifest version="9999.0.0">
<packages>
<package hash="+LXvjiaPkeYDLHoNKlf9qbJwvnk=" name="update.gz" size="67546213" required="true"/>
</packages>
<actions>
<action event="postinstall" DisplayVersion="9999.0.0" sha256="0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=" needsadmin="false" IsDeltaPayload="true" />
</actions>
</manifest>
</updatecheck>
</app>
</response>
`
)
func TestOmahaRequestUpdateCheck(t *testing.T) {
v := Request{}
xml.Unmarshal([]byte(SampleRequest), &v)
v, err := ParseRequest("", strings.NewReader(sampleRequest))
if err != nil {
t.Fatalf("ParseRequest failed: %v", err)
}
if v.OS.Version != "Indy" {
t.Error("Unexpected version", v.OS.Version)
@ -80,6 +107,63 @@ func TestOmahaRequestUpdateCheck(t *testing.T) {
}
}
func TestOmahaResponseWithUpdate(t *testing.T) {
parsed, err := ParseResponse("", strings.NewReader(sampleResponse))
if err != nil {
t.Fatalf("ParseResponse failed: %v", err)
}
expected := &Response{
XMLName: xml.Name{Local: "response"},
Protocol: "3.0",
DayStart: DayStart{ElapsedSeconds: "49008"},
Apps: []*AppResponse{&AppResponse{
ID: "{87efface-864d-49a5-9bb3-4b050a7c227a}",
Status: AppOK,
Ping: &PingResponse{"ok"},
UpdateCheck: &UpdateResponse{
Status: UpdateOK,
URLs: []*URL{&URL{
CodeBase: "http://kam:8080/static/",
}},
Manifest: &Manifest{
Version: "9999.0.0",
Packages: []*Package{&Package{
SHA1: "+LXvjiaPkeYDLHoNKlf9qbJwvnk=",
Name: "update.gz",
Size: 67546213,
Required: true,
}},
Actions: []*Action{&Action{
Event: "postinstall",
DisplayVersion: "9999.0.0",
SHA256: "0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=",
IsDeltaPayload: true,
}},
},
},
}},
}
if !reflect.DeepEqual(parsed, expected) {
t.Errorf("parsed != expected\n%s\n%s", parsed, expected)
}
}
func TestOmahaResponsAsRequest(t *testing.T) {
_, err := ParseRequest("", strings.NewReader(sampleResponse))
if err == nil {
t.Fatal("ParseRequest successfully parsed a response")
}
}
func TestOmahaRequestAsResponse(t *testing.T) {
_, err := ParseResponse("", strings.NewReader(sampleRequest))
if err == nil {
t.Fatal("ParseResponse successfully parsed a request")
}
}
func ExampleNewResponse() {
response := NewResponse()
app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}", "ok")