Skip to content

Commit 7d868f1

Browse files
authored
[Enhancement] Restrict internal response body types (#416)
[Enhancement] Restrict internal response body types What this PR does / why we need it Restrict extract method arguments to io.Reader Make Result.Body a byte slice Make pagination handling less cryptic Which issue this PR fixes Resolves #408 Special notes for your reviewer The reason why Result.Body is []bytes - we can read and unmarshall it multiple times. Readers are expected to be read once. Progress: lint passing pagination unit tests passing other unit tests passing (here previous implementation iterations died) acceptance tests passing Reviewed-by: Aloento <None> Reviewed-by: Anton Kachurin <None> Reviewed-by: Artem Lifshits <None>
1 parent 791d6bc commit 7d868f1

38 files changed

Lines changed: 372 additions & 372 deletions

File tree

internal/extract/doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// Package extract contains functions for extracting JSON results into given structure or slice pointers.
2-
// Those are wrappers over `json.Marshall` and `json.Unmarshall` functions with additional validation built it
2+
// Those are wrappers over `json.Marshal` and `json.Unmarshal` functions with additional validation built it
33
package extract

internal/extract/json.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ package extract
33
import (
44
"bytes"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
8-
"net/http"
99
"reflect"
1010
)
1111

12-
func intoPtr(body, to interface{}, label string) error {
12+
func intoPtr(body io.Reader, to interface{}, label string) error {
1313
if label == "" {
1414
return Into(body, &to)
1515
}
@@ -98,6 +98,7 @@ func intoPtr(body, to interface{}, label string) error {
9898
return err
9999
}
100100

101+
// JsonMarshal marshals input to bytes via buffer with disabled HTML escaping.
101102
func JsonMarshal(t interface{}) ([]byte, error) {
102103
buffer := &bytes.Buffer{}
103104
enc := json.NewEncoder(buffer)
@@ -106,24 +107,31 @@ func JsonMarshal(t interface{}) ([]byte, error) {
106107
return buffer.Bytes(), err
107108
}
108109

109-
func Into(body interface{}, to interface{}) error {
110-
if raw, ok := body.(http.Response); ok {
111-
body = raw.Body
110+
// Into parses input as JSON and convert to a structure.
111+
func Into(body io.Reader, to interface{}) error {
112+
if closer, ok := body.(io.ReadCloser); ok {
113+
defer closer.Close()
112114
}
113115

114-
if reader, ok := body.(io.ReadCloser); ok {
115-
defer reader.Close()
116-
return json.NewDecoder(reader).Decode(to)
117-
}
116+
// json.NewDecoder(..).Decode() replaced with reading whole body for better
117+
// error tracing and debug simplicity
118+
// TODO: compare this solution to original one in terms of performance
118119

119-
// TODO: remove this branch in pager refactoring
120-
b, err := JsonMarshal(body)
120+
byteBody, err := io.ReadAll(body)
121121
if err != nil {
122-
return err
122+
return fmt.Errorf("error reading from stream: %w", err)
123123
}
124-
err = json.Unmarshal(b, to)
125124

126-
return err
125+
if len(byteBody) == 0 {
126+
return nil // empty body - nothing to extract
127+
}
128+
129+
err = json.Unmarshal(byteBody, to)
130+
if err != nil && !errors.Is(err, io.EOF) {
131+
return fmt.Errorf("error extracting %s into %T: %w", byteBody, to, err)
132+
}
133+
134+
return nil
127135
}
128136

129137
func typeCheck(to interface{}, kind reflect.Kind) error {
@@ -140,7 +148,7 @@ func typeCheck(to interface{}, kind reflect.Kind) error {
140148
}
141149

142150
// IntoStructPtr will unmarshal the given body into the provided Struct.
143-
func IntoStructPtr(body, to interface{}, label string) error {
151+
func IntoStructPtr(body io.Reader, to interface{}, label string) error {
144152
err := typeCheck(to, reflect.Struct)
145153
if err != nil {
146154
return err
@@ -150,7 +158,7 @@ func IntoStructPtr(body, to interface{}, label string) error {
150158
}
151159

152160
// IntoSlicePtr will unmarshal the provided body into the provided Slice.
153-
func IntoSlicePtr(body, to interface{}, label string) error {
161+
func IntoSlicePtr(body io.Reader, to interface{}, label string) error {
154162
err := typeCheck(to, reflect.Slice)
155163
if err != nil {
156164
return err

internal/extract/json_test.go

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,48 @@ func randomString(prefix string, n int) string {
2323
return prefix + string(bytes)
2424
}
2525

26+
type localCloser struct {
27+
*bytes.Reader
28+
29+
closed bool
30+
}
31+
32+
func (lc *localCloser) Close() error {
33+
lc.closed = true
34+
return nil
35+
}
36+
2637
func TestInto(t *testing.T) {
2738
key := "data_key"
2839
value := randomString("v-", 20)
2940

3041
expected := map[string]string{key: value}
3142

32-
cases := map[string]interface{}{
33-
"map": map[string]string{key: value},
34-
"struct": struct {
35-
DataKey string `json:"data_key"`
36-
}{value},
37-
"struct with other fields": struct {
38-
DataKey string `json:"data_key"`
39-
DataKey2 string `json:"-"`
40-
}{value, "difgljdfgn"},
41-
"io.Reader": bytes.NewReader([]byte(fmt.Sprintf(`{ "data_key": "%s"}`, value))),
42-
}
43+
t.Run("io.Reader", func(t *testing.T) {
44+
t.Parallel()
4345

44-
for name, source := range cases {
45-
source := source // avoid issues with parallel tests
46-
expectedValue := expected[key]
46+
data := bytes.NewReader([]byte(fmt.Sprintf(`{ "data_key": "%s"}`, value)))
4747

48-
t.Run(name, func(t *testing.T) {
49-
t.Parallel()
48+
actual := make(map[string]string)
49+
err := Into(data, &actual)
5050

51-
actual := make(map[string]string)
52-
err := Into(source, &actual)
51+
assert.NoError(t, err) // not exiting after one fail
52+
assert.EqualValues(t, expected[key], actual[key])
53+
})
5354

54-
assert.NoError(t, err) // not exiting after one fail
55-
assert.EqualValues(t, expectedValue, actual[key])
56-
})
57-
}
55+
t.Run("io.ReadCloser", func(t *testing.T) {
56+
t.Parallel()
57+
58+
data := bytes.NewReader([]byte(fmt.Sprintf(`{ "data_key": "%s"}`, value)))
59+
closer := &localCloser{Reader: data}
60+
61+
actual := make(map[string]string)
62+
err := Into(closer, &actual)
63+
64+
assert.NoError(t, err) // not exiting after one fail
65+
assert.EqualValues(t, expected[key], actual[key])
66+
assert.True(t, closer.closed)
67+
})
5868
}
5969

6070
type TestDataType struct {

openstack/blockstorage/extensions/quotasets/testing/fixtures.go

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,41 +36,41 @@ var getExpectedQuotaSet = quotasets.QuotaSet{
3636

3737
var getUsageExpectedJSONBody = `
3838
{
39-
"quota_set" : {
40-
"id": "555544443333222211110000ffffeeee",
41-
"volumes" : {
42-
"in_use": 15,
43-
"limit": 16,
44-
"reserved": 17
45-
},
46-
"snapshots" : {
47-
"in_use": 18,
48-
"limit": 19,
49-
"reserved": 20
50-
},
51-
"gigabytes" : {
52-
"in_use": 21,
53-
"limit": 22,
54-
"reserved": 23
55-
},
56-
"per_volume_gigabytes" : {
57-
"in_use": 24,
58-
"limit": 25,
59-
"reserved": 26
60-
},
61-
"backups" : {
62-
"in_use": 27,
63-
"limit": 28,
64-
"reserved": 29
65-
},
66-
"backup_gigabytes" : {
67-
"in_use": 30,
68-
"limit": 31,
69-
"reserved": 32
70-
}
71-
}
72-
}
73-
}`
39+
"quota_set": {
40+
"id": "555544443333222211110000ffffeeee",
41+
"volumes": {
42+
"in_use": 15,
43+
"limit": 16,
44+
"reserved": 17
45+
},
46+
"snapshots": {
47+
"in_use": 18,
48+
"limit": 19,
49+
"reserved": 20
50+
},
51+
"gigabytes": {
52+
"in_use": 21,
53+
"limit": 22,
54+
"reserved": 23
55+
},
56+
"per_volume_gigabytes": {
57+
"in_use": 24,
58+
"limit": 25,
59+
"reserved": 26
60+
},
61+
"backups": {
62+
"in_use": 27,
63+
"limit": 28,
64+
"reserved": 29
65+
},
66+
"backup_gigabytes": {
67+
"in_use": 30,
68+
"limit": 31,
69+
"reserved": 32
70+
}
71+
}
72+
}
73+
`
7474

7575
var getUsageExpectedQuotaSet = quotasets.QuotaUsageSet{
7676
ID: FirstTenantID,

openstack/blockstorage/v1/snapshots/results.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/opentelekomcloud/gophertelekomcloud"
8+
"github.com/opentelekomcloud/gophertelekomcloud/openstack/common/metadata"
89
"github.com/opentelekomcloud/gophertelekomcloud/pagination"
910
)
1011

@@ -109,11 +110,7 @@ type UpdateMetadataResult struct {
109110

110111
// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
111112
func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
112-
if r.Err != nil {
113-
return nil, r.Err
114-
}
115-
m := r.Body.(map[string]interface{})["metadata"]
116-
return m.(map[string]interface{}), nil
113+
return metadata.Extract(r.BodyReader())
117114
}
118115

119116
type commonResult struct {

openstack/blockstorage/v2/snapshots/results.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/opentelekomcloud/gophertelekomcloud"
8+
"github.com/opentelekomcloud/gophertelekomcloud/openstack/common/metadata"
89
"github.com/opentelekomcloud/gophertelekomcloud/pagination"
910
)
1011

@@ -99,11 +100,7 @@ type UpdateMetadataResult struct {
99100

100101
// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
101102
func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
102-
if r.Err != nil {
103-
return nil, r.Err
104-
}
105-
m := r.Body.(map[string]interface{})["metadata"]
106-
return m.(map[string]interface{}), nil
103+
return metadata.Extract(r.BodyReader())
107104
}
108105

109106
type commonResult struct {

openstack/blockstorage/v3/snapshots/results.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/opentelekomcloud/gophertelekomcloud"
8+
"github.com/opentelekomcloud/gophertelekomcloud/openstack/common/metadata"
89
"github.com/opentelekomcloud/gophertelekomcloud/pagination"
910
)
1011

@@ -111,11 +112,7 @@ type UpdateMetadataResult struct {
111112

112113
// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
113114
func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
114-
if r.Err != nil {
115-
return nil, r.Err
116-
}
117-
m := r.Body.(map[string]interface{})["metadata"]
118-
return m.(map[string]interface{}), nil
115+
return metadata.Extract(r.BodyReader())
119116
}
120117

121118
type commonResult struct {

openstack/bms/v2/flavors/results.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (page FlavorPage) IsEmpty() (bool, error) {
8888
// next page of results.
8989
func (page FlavorPage) NextPageURL() (string, error) {
9090
var res []golangsdk.Link
91-
err := extract.IntoSlicePtr(page.Result.Body, &res, "flavors_links")
91+
err := extract.IntoSlicePtr(page.Result.BodyReader(), &res, "flavors_links")
9292
if err != nil {
9393
return "", err
9494
}
@@ -99,6 +99,6 @@ func (page FlavorPage) NextPageURL() (string, error) {
9999
// from the List operation.
100100
func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
101101
var res []Flavor
102-
err := extract.IntoSlicePtr(r.(FlavorPage).Result.Body, &res, "flavors")
102+
err := extract.IntoSlicePtr(r.(FlavorPage).Result.BodyReader(), &res, "flavors")
103103
return res, err
104104
}

openstack/bms/v2/keypairs/results.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) {
3333
KeyPair KeyPair `json:"keypair"`
3434
}
3535

36-
err := extract.IntoSlicePtr(r.(KeyPairPage).Result.Body, &res, "keypairs")
36+
err := extract.IntoSlicePtr(r.(KeyPairPage).Result.BodyReader(), &res, "keypairs")
3737
results := make([]KeyPair, len(res))
3838

3939
for i, pair := range res {

openstack/bms/v2/nics/results.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type NicPage struct {
3636
func (r NicPage) NextPageURL() (string, error) {
3737
var res []golangsdk.Link
3838

39-
err := extract.IntoSlicePtr(r.Body, &res, "interfaceAttachments_links")
39+
err := extract.IntoSlicePtr(r.BodyReader(), &res, "interfaceAttachments_links")
4040
if err != nil {
4141
return "", err
4242
}
@@ -55,6 +55,6 @@ func (r NicPage) IsEmpty() (bool, error) {
5555
// a generic collection is mapped into a relevant slice.
5656
func ExtractNics(r pagination.Page) ([]Nic, error) {
5757
var res []Nic
58-
err := extract.IntoSlicePtr(r.(NicPage).Result.Body, &res, "interfaceAttachments")
58+
err := extract.IntoSlicePtr(r.(NicPage).BodyReader(), &res, "interfaceAttachments")
5959
return res, err
6060
}

0 commit comments

Comments
 (0)