1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package storage
16
17 import (
18 "context"
19 "encoding/base64"
20 "encoding/json"
21 "fmt"
22 "io/ioutil"
23 "net/url"
24 "os"
25 "strconv"
26 "strings"
27 "testing"
28 "time"
29
30 storage_v1_tests "cloud.google.com/go/storage/internal/test/conformance"
31 "github.com/google/go-cmp/cmp"
32 "google.golang.org/api/option"
33 "google.golang.org/protobuf/encoding/protojson"
34 )
35
36 func TestPostPolicyV4Conformance(t *testing.T) {
37 oldUTCNow := utcNow
38 defer func() {
39 utcNow = oldUTCNow
40 }()
41
42 googleAccessID, privateKey, testFiles := parseFiles(t)
43
44 for _, testFile := range testFiles {
45 for _, tc := range testFile.PostPolicyV4Tests {
46 t.Run(tc.Description, func(t *testing.T) {
47 pin := tc.PolicyInput
48 utcNow = func() time.Time {
49 return time.Unix(pin.GetTimestamp().GetSeconds(), 0).UTC()
50 }
51
52 var style URLStyle
53 switch pin.UrlStyle {
54 case storage_v1_tests.UrlStyle_PATH_STYLE:
55 style = PathStyle()
56 case storage_v1_tests.UrlStyle_VIRTUAL_HOSTED_STYLE:
57 style = VirtualHostedStyle()
58 case storage_v1_tests.UrlStyle_BUCKET_BOUND_HOSTNAME:
59 style = BucketBoundHostname(pin.BucketBoundHostname)
60 }
61
62 var conditions []PostPolicyV4Condition
63
64 pinConds := pin.Conditions
65 if pinConds != nil {
66 if clr := pinConds.ContentLengthRange; len(clr) > 0 {
67 conditions = append(conditions, ConditionContentLengthRange(uint64(clr[0]), uint64(clr[1])))
68 }
69 if sw := pinConds.StartsWith; len(sw) > 0 {
70 conditions = append(conditions, ConditionStartsWith(sw[0], sw[1]))
71 }
72 }
73
74 metadata := make(map[string]string, len(pin.Fields))
75 for key, value := range pin.Fields {
76 if strings.HasPrefix(key, "x-goog-meta") {
77 metadata[key] = value
78 }
79 }
80
81 got, err := GenerateSignedPostPolicyV4(pin.Bucket, pin.Object, &PostPolicyV4Options{
82 GoogleAccessID: googleAccessID,
83 PrivateKey: []byte(privateKey),
84 Expires: utcNow().Add(time.Duration(pin.Expiration) * time.Second),
85 Style: style,
86 Insecure: pin.Scheme == "http",
87 Conditions: conditions,
88 Fields: &PolicyV4Fields{
89 ACL: pin.Fields["acl"],
90 CacheControl: pin.Fields["cache-control"],
91 ContentEncoding: pin.Fields["content-encoding"],
92 ContentDisposition: pin.Fields["content-disposition"],
93 ContentType: pin.Fields["content-type"],
94 Metadata: metadata,
95 RedirectToURLOnSuccess: strings.TrimSpace(pin.Fields["success_action_redirect"]),
96 StatusCodeOnSuccess: mustInt(t, pin.Fields["success_action_status"]),
97 },
98 })
99 if err != nil {
100 t.Fatal(err)
101 }
102 want := tc.PolicyOutput
103
104 switch wantURL, err := url.Parse(want.Url); {
105 case err != nil:
106 t.Errorf("Failed to parse want.Url: %v", err)
107
108 default:
109
110 wantURL.RawQuery = wantURL.Query().Encode()
111 if diff := cmp.Diff(got.URL, wantURL.String()); diff != "" {
112 t.Errorf("URL mismatch: got - want +\n%s", diff)
113 }
114 }
115
116 gotPolicy := b64Decode(t, got.Fields["policy"], "got")
117 wantPolicy := want.ExpectedDecodedPolicy
118 if diff := cmp.Diff(gotPolicy, wantPolicy); diff != "" {
119 t.Fatalf("Policy mismatch: got - want +\n%s", diff)
120 }
121 })
122 }
123 }
124 }
125
126 func b64Decode(t *testing.T, b64Str, name string) string {
127 dec, err := base64.StdEncoding.DecodeString(b64Str)
128 if err != nil {
129 t.Fatalf("%q: Base64 decoding failed: %v", name, err)
130 }
131 return string(dec)
132 }
133
134 func mustInt(t *testing.T, s string) int {
135 if s == "" {
136 return 0
137 }
138 i, err := strconv.ParseInt(s, 10, 64)
139 if err != nil {
140 t.Fatal(err)
141 }
142 return int(i)
143 }
144
145 func TestSigningV4Conformance(t *testing.T) {
146 oldUTCNow := utcNow
147 defer func() {
148 utcNow = oldUTCNow
149 }()
150
151 googleAccessID, privateKey, testFiles := parseFiles(t)
152
153 for _, testFile := range testFiles {
154 for _, tc := range testFile.SigningV4Tests {
155 t.Run(tc.Description, func(t *testing.T) {
156 utcNow = func() time.Time {
157 return time.Unix(tc.Timestamp.Seconds, 0).UTC()
158 }
159
160 qp := url.Values{}
161 if tc.QueryParameters != nil {
162 for k, v := range tc.QueryParameters {
163 qp.Add(k, v)
164 }
165 }
166
167 var style URLStyle
168 switch tc.UrlStyle {
169 case storage_v1_tests.UrlStyle_PATH_STYLE:
170 style = PathStyle()
171 case storage_v1_tests.UrlStyle_VIRTUAL_HOSTED_STYLE:
172 style = VirtualHostedStyle()
173 case storage_v1_tests.UrlStyle_BUCKET_BOUND_HOSTNAME:
174 style = BucketBoundHostname(tc.BucketBoundHostname)
175 }
176
177 t.Setenv("STORAGE_EMULATOR_HOST", tc.EmulatorHostname)
178
179 opts := []option.ClientOption{option.WithoutAuthentication()}
180 if tc.ClientEndpoint != "" {
181 opts = append(opts, option.WithEndpoint(tc.ClientEndpoint))
182 }
183 if tc.UniverseDomain != "" {
184 opts = append(opts, option.WithUniverseDomain(tc.UniverseDomain))
185 }
186
187 c, err := NewClient(context.Background(), opts...)
188 if err != nil {
189 t.Fatalf("NewClient: %v", err)
190 }
191
192 gotURL, err := c.Bucket(tc.Bucket).SignedURL(tc.Object, &SignedURLOptions{
193 GoogleAccessID: googleAccessID,
194 PrivateKey: []byte(privateKey),
195 Method: tc.Method,
196 Expires: utcNow().Add(time.Duration(tc.Expiration) * time.Second),
197 Scheme: SigningSchemeV4,
198 Headers: headersAsSlice(tc.Headers),
199 QueryParameters: qp,
200 Style: style,
201 Insecure: tc.Scheme == "http",
202 Hostname: tc.Hostname,
203 })
204 if err != nil {
205 t.Fatal(err)
206 }
207 wantURL, err := url.Parse(tc.ExpectedUrl)
208 if err != nil {
209 t.Fatal(err)
210 }
211
212 wantURL.RawQuery = wantURL.Query().Encode()
213
214 if gotURL != wantURL.String() {
215 t.Fatalf("\nwant:\t%s\ngot:\t%s", wantURL.String(), gotURL)
216 }
217 })
218 }
219 }
220 }
221
222 func headersAsSlice(m map[string]string) []string {
223 var s []string
224 for k, v := range m {
225 s = append(s, fmt.Sprintf("%s:%s", k, v))
226 }
227 return s
228 }
229
230 func parseFiles(t *testing.T) (googleAccessID, privateKey string, testFiles []*storage_v1_tests.TestFile) {
231 dir := "internal/test/conformance"
232
233 inBytes, err := ioutil.ReadFile(dir + "/service-account")
234 if err != nil {
235 t.Fatal(err)
236 }
237 serviceAccount := map[string]string{}
238 if err := json.Unmarshal(inBytes, &serviceAccount); err != nil {
239 t.Fatal(err)
240 }
241 googleAccessID = serviceAccount["client_email"]
242 privateKey = serviceAccount["private_key"]
243
244 files, err := os.ReadDir(dir)
245 if err != nil {
246 t.Fatal(err)
247 }
248
249 for _, f := range files {
250 if !strings.HasSuffix(f.Name(), ".json") {
251 continue
252 }
253
254 inBytes, err := os.ReadFile(dir + "/" + f.Name())
255 if err != nil {
256 t.Fatalf("%s: %v", f.Name(), err)
257 }
258
259 testFile := new(storage_v1_tests.TestFile)
260 if err := protojson.Unmarshal(inBytes, testFile); err != nil {
261 t.Fatalf("unmarshalling %s: %v", f.Name(), err)
262 }
263 testFiles = append(testFiles, testFile)
264 }
265 return
266 }
267
View as plain text