Skip to content

Commit 201364e

Browse files
authored
fix: Add required fields to Omaha v4 response (#269)
* fix: Add required `size` field to Omaha v4 response Ref: https://source.chromium.org/chromium/chromium/src/+/cf775224f2c9a85364d1632e1f57e1023fa1d320 * feat: Tighten up response object validation * feat: Add further response object validations The response supports just 3 operation types (`download`, `puff` and `crx3`), so let's make sure only that can be used * fix: `size` should be placed at the same level as `type`
1 parent bcf8071 commit 201364e

File tree

5 files changed

+275
-31
lines changed

5 files changed

+275
-31
lines changed

controller/controller.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/url"
99
"os"
10+
"strconv"
1011
"strings"
1112
"time"
1213

@@ -76,6 +77,21 @@ func initExtensionUpdatesFromDynamoDB() {
7677
SHA256: *item["SHA256"].S,
7778
Title: *item["Title"].S,
7879
Version: *item["Version"].S,
80+
Size: 1, // Required field as per Omaha v4 spec (must be >0); its correctness is NOT verified by the browser
81+
}
82+
83+
// Add Size field if present in DynamoDB
84+
if sizeItem := item["Size"]; sizeItem != nil && sizeItem.N != nil {
85+
size, err := strconv.ParseUint(*sizeItem.N, 10, 64)
86+
if err != nil {
87+
log.Printf("failed to parse Size %v\n", err)
88+
sentry.CaptureException(err)
89+
} else {
90+
if size == 0 {
91+
size = 1
92+
}
93+
ext.Size = size
94+
}
7995
}
8096

8197
if plist := item["PatchList"]; plist != nil {
@@ -89,7 +105,6 @@ func initExtensionUpdatesFromDynamoDB() {
89105
}
90106

91107
AllExtensionsMap.Store(id, ext)
92-
93108
}
94109
}
95110

extension/extension.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Extension struct {
2222
SHA256 string
2323
Title string
2424
URL string
25+
Size uint64
2526
Blacklisted bool
2627
Status string
2728
PatchList map[string]*PatchInfo

omaha/v4/protocol_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func TestResponseMarshalJSONV40(t *testing.T) {
5757
ID: "test-app-id",
5858
Version: "1.0.0",
5959
SHA256: "test-sha256",
60+
Size: 100,
6061
PatchList: map[string]*extension.PatchInfo{
6162
"test-fp": {
6263
Hashdiff: "test-hash-diff",

omaha/v4/response.go

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package v4
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"strings"
67
"time"
78

89
"github.com/brave/go-update/extension"
10+
"github.com/go-playground/validator/v10"
911
)
1012

1113
// GetElapsedDays calculates elapsed days since Jan 1, 2007
@@ -28,20 +30,21 @@ func GetUpdateStatus(extension extension.Extension) string {
2830
// MarshalJSON encodes the extension list into response JSON
2931
func (r *UpdateResponse) MarshalJSON() ([]byte, error) {
3032
type URL struct {
31-
URL string `json:"url"`
33+
URL string `json:"url" validate:"required"`
3234
}
3335
type Out struct {
34-
SHA256 string `json:"sha256"`
36+
SHA256 string `json:"sha256" validate:"required"`
3537
}
3638
type In struct {
37-
SHA256 string `json:"sha256"`
39+
SHA256 string `json:"sha256" validate:"required"`
3840
}
3941
type Operation struct {
40-
Type string `json:"type"`
41-
Out *Out `json:"out,omitempty"`
42-
In *In `json:"in,omitempty"`
43-
URLs []URL `json:"urls,omitempty"`
44-
Previous *In `json:"previous,omitempty"`
42+
Type string `json:"type" validate:"required,oneof=download puff crx3"`
43+
Out *Out `json:"out,omitempty" validate:"omitempty,required_if=Type download"`
44+
In *In `json:"in,omitempty" validate:"omitempty,required_if=Type crx3"`
45+
URLs []URL `json:"urls,omitempty" validate:"omitempty,required_if=Type download,dive"`
46+
Previous *In `json:"previous,omitempty" validate:"omitempty,required_if=Type puff"`
47+
Size uint64 `json:"size,omitempty" validate:"omitempty,required_if=Type download,gt=0"`
4548
}
4649
type Pipeline struct {
4750
PipelineID string `json:"pipeline_id"`
@@ -79,7 +82,15 @@ func (r *UpdateResponse) MarshalJSON() ([]byte, error) {
7982
},
8083
}
8184

85+
// Create validator instance
86+
validate := validator.New()
87+
8288
for _, ext := range *r {
89+
// Check if SHA256 is empty
90+
if ext.SHA256 == "" {
91+
return nil, fmt.Errorf("extension %s has empty SHA256", ext.ID)
92+
}
93+
8394
app := App{AppID: ext.ID, Status: "ok"}
8495
updateStatus := GetUpdateStatus(ext)
8596
app.UpdateCheck = UpdateCheck{Status: updateStatus}
@@ -97,6 +108,11 @@ func (r *UpdateResponse) MarshalJSON() ([]byte, error) {
97108
// Add diff pipeline if patch is available (diff pipeline should come first)
98109
if ext.FP != "" && ext.PatchList != nil {
99110
if patchInfo, ok := ext.PatchList[ext.FP]; ok {
111+
// Check if hashdiff is empty
112+
if patchInfo.Hashdiff == "" {
113+
return nil, fmt.Errorf("extension %s has empty Hashdiff", ext.ID)
114+
}
115+
100116
fpPrefix := ext.FP
101117
if len(ext.FP) >= 8 {
102118
fpPrefix = ext.FP[:8]
@@ -105,22 +121,49 @@ func (r *UpdateResponse) MarshalJSON() ([]byte, error) {
105121
patchURL := "https://" + extension.GetS3ExtensionBucketHost(ext.ID) + "/release/" +
106122
ext.ID + "/patches/" + ext.SHA256 + "/" + ext.FP + ".puff"
107123

124+
// Create the Out struct for diff pipeline
125+
diffOut := &Out{
126+
SHA256: patchInfo.Hashdiff,
127+
}
128+
129+
// Create URLs for diff pipeline
130+
diffURLs := []URL{{URL: patchURL}}
131+
132+
// Create In structs
133+
previousIn := &In{SHA256: ext.FP}
134+
crx3In := &In{SHA256: ext.SHA256}
135+
136+
// Create operations for diff pipeline
137+
diffDownloadOp := Operation{
138+
Type: "download",
139+
Out: diffOut,
140+
URLs: diffURLs,
141+
Size: normalizeSize(uint64(patchInfo.Sizediff)),
142+
}
143+
144+
puffOp := Operation{
145+
Type: "puff",
146+
Previous: previousIn,
147+
}
148+
149+
crx3Op := Operation{
150+
Type: "crx3",
151+
In: crx3In,
152+
}
153+
154+
// Validate all operations
155+
for _, op := range []Operation{diffDownloadOp, puffOp, crx3Op} {
156+
if err := validate.Struct(op); err != nil {
157+
return nil, fmt.Errorf("%s operation validation failed for extension %s: %v", op.Type, ext.ID, err)
158+
}
159+
}
160+
108161
diffPipeline := Pipeline{
109162
PipelineID: diffPipelineID,
110163
Operations: []Operation{
111-
{
112-
Type: "download",
113-
Out: &Out{SHA256: patchInfo.Hashdiff},
114-
URLs: []URL{{URL: patchURL}},
115-
},
116-
{
117-
Type: "puff",
118-
Previous: &In{SHA256: ext.FP},
119-
},
120-
{
121-
Type: "crx3",
122-
In: &In{SHA256: ext.SHA256},
123-
},
164+
diffDownloadOp,
165+
puffOp,
166+
crx3Op,
124167
},
125168
}
126169

@@ -129,18 +172,38 @@ func (r *UpdateResponse) MarshalJSON() ([]byte, error) {
129172
}
130173

131174
// Add full pipeline as fallback (always add as the last pipeline)
175+
out := &Out{
176+
SHA256: ext.SHA256,
177+
}
178+
179+
urls := []URL{{URL: url}}
180+
mainCrx3In := &In{SHA256: ext.SHA256}
181+
182+
// Create operations for main pipeline
183+
mainDownloadOp := Operation{
184+
Type: "download",
185+
Out: out,
186+
URLs: urls,
187+
Size: normalizeSize(ext.Size),
188+
}
189+
190+
mainCrx3Op := Operation{
191+
Type: "crx3",
192+
In: mainCrx3In,
193+
}
194+
195+
// Validate all operations in the main pipeline
196+
for _, op := range []Operation{mainDownloadOp, mainCrx3Op} {
197+
if err := validate.Struct(op); err != nil {
198+
return nil, fmt.Errorf("%s operation validation failed for extension %s: %v", op.Type, ext.ID, err)
199+
}
200+
}
201+
132202
pipeline := Pipeline{
133203
PipelineID: "direct_full",
134204
Operations: []Operation{
135-
{
136-
Type: "download",
137-
Out: &Out{SHA256: ext.SHA256},
138-
URLs: []URL{{URL: url}},
139-
},
140-
{
141-
Type: "crx3",
142-
In: &In{SHA256: ext.SHA256},
143-
},
205+
mainDownloadOp,
206+
mainCrx3Op,
144207
},
145208
}
146209

@@ -156,3 +219,10 @@ func (r *UpdateResponse) MarshalJSON() ([]byte, error) {
156219

157220
return json.Marshal(jsonResponse)
158221
}
222+
223+
func normalizeSize(size uint64) uint64 {
224+
if size == 0 {
225+
return 1
226+
}
227+
return size
228+
}

0 commit comments

Comments
 (0)