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:
parent
6198ba9443
commit
a6290c1b4f
5 changed files with 252 additions and 16 deletions
|
@ -33,25 +33,18 @@ func (o *OmahaHandler) ServeHTTP(w http.ResponseWriter, httpReq *http.Request) {
|
||||||
|
|
||||||
// A request over 1M in size is certainly bogus.
|
// A request over 1M in size is certainly bogus.
|
||||||
reader := http.MaxBytesReader(w, httpReq.Body, 1024*1024)
|
reader := http.MaxBytesReader(w, httpReq.Body, 1024*1024)
|
||||||
|
contentType := httpReq.Header.Get("Content-Type")
|
||||||
decoder := xml.NewDecoder(reader)
|
omahaReq, err := ParseRequest(contentType, reader)
|
||||||
var omahaReq Request
|
if err != nil {
|
||||||
if err := decoder.Decode(&omahaReq); err != nil {
|
log.Printf("omaha: Failed parsing request: %v", err)
|
||||||
log.Printf("omaha: Failed decoding XML: %v", err)
|
http.Error(w, "Bad Omaha Request", http.StatusBadRequest)
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httpStatus := 0
|
httpStatus := 0
|
||||||
omahaResp := NewResponse()
|
omahaResp := NewResponse()
|
||||||
for _, appReq := range omahaReq.Apps {
|
for _, appReq := range omahaReq.Apps {
|
||||||
appResp := o.serveApp(omahaResp, httpReq, &omahaReq, appReq)
|
appResp := o.serveApp(omahaResp, httpReq, omahaReq, appReq)
|
||||||
if appResp.Status == AppOK {
|
if appResp.Status == AppOK {
|
||||||
// HTTP is ok if any app is ok.
|
// HTTP is ok if any app is ok.
|
||||||
httpStatus = http.StatusOK
|
httpStatus = http.StatusOK
|
||||||
|
|
71
omaha/parse.go
Normal file
71
omaha/parse.go
Normal 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
55
omaha/parse_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ package omaha
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request sent by the Omaha client
|
// 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 {
|
func (r *Request) AddApp(id, version string) *AppRequest {
|
||||||
a := &AppRequest{ID: id, Version: version}
|
a := &AppRequest{ID: id, Version: version}
|
||||||
r.Apps = append(r.Apps, a)
|
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 {
|
type DayStart struct {
|
||||||
ElapsedSeconds string `xml:"elapsed_seconds,attr"`
|
ElapsedSeconds string `xml:"elapsed_seconds,attr"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,13 @@ package omaha
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"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">
|
<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>
|
<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" >
|
<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>
|
</app>
|
||||||
</request>
|
</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) {
|
func TestOmahaRequestUpdateCheck(t *testing.T) {
|
||||||
v := Request{}
|
v, err := ParseRequest("", strings.NewReader(sampleRequest))
|
||||||
xml.Unmarshal([]byte(SampleRequest), &v)
|
if err != nil {
|
||||||
|
t.Fatalf("ParseRequest failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if v.OS.Version != "Indy" {
|
if v.OS.Version != "Indy" {
|
||||||
t.Error("Unexpected version", v.OS.Version)
|
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() {
|
func ExampleNewResponse() {
|
||||||
response := NewResponse()
|
response := NewResponse()
|
||||||
app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}", "ok")
|
app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}", "ok")
|
||||||
|
|
Loading…
Reference in a new issue