~rvb/gwacl/image-fix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
// Copyright 2013 Canonical Ltd.  This software is licensed under the
// GNU Lesser General Public License version 3 (see the file COPYING).

package gwacl

// This file contains the operations necessary to work with the Azure
// file storage API.  For more details, see
// http://msdn.microsoft.com/en-us/library/windowsazure/dd179355.aspx

// TODO Improve function documentation: the Go documentation convention is for
// function documentation to start out with the name of the function. This may
// have special significance for godoc.

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "net/url"
    "sort"
    "strings"
    "time"
)

var headers_to_sign = []string{
    "Content-Encoding",
    "Content-Language",
    "Content-Length",
    "Content-MD5",
    "Content-Type",
    "Date",
    "If-Modified-Since",
    "If-Match",
    "If-None-Match",
    "If-Unmodified-Since",
    "Range",
}

// Add the Authorization: header to a Request.
func signRequest(req *http.Request, account_name, account_key string) {
    header := composeAuthHeader(req, account_name, account_key)
    req.Header.Set("Authorization", header)
}

// Calculate the value required for an Authorization header.
func composeAuthHeader(req *http.Request, account_name, account_key string) string {
    signable := composeStringToSign(req, account_name)
    // Allegedly, this is already UTF8 encoded.
    decoded_key, err := base64.StdEncoding.DecodeString(account_key)
    if err != nil {
        panic(fmt.Errorf("invalid account key: %s", err))
    }
    hash := hmac.New(sha256.New, decoded_key)
    _, err = hash.Write([]byte(signable))
    if err != nil {
        panic(fmt.Errorf("failed to write hash: %s", err))
    }
    var hashed []byte
    hashed = hash.Sum(hashed)
    b64_hashed := base64.StdEncoding.EncodeToString(hashed)
    return fmt.Sprintf("SharedKey %s:%s", account_name, b64_hashed)
}

// Calculate the string that needs to be HMAC signed.  It is comprised of
// the headers in headers_to_sign, x-ms-* headers and the URI params.
func composeStringToSign(req *http.Request, account_name string) string {
    // TODO: whitespace should be normalised in value strings.
    return fmt.Sprintf(
        "%s\n%s%s%s", req.Method, composeHeaders(req),
        composeCanonicalizedHeaders(req),
        composeCanonicalizedResource(req, account_name))
}

// toLowerKeys lower cases all map keys. If two keys exist, that differ
// by the case of their keys, the values will be concatenated.
func toLowerKeys(values url.Values) map[string][]string {
    m := make(map[string][]string)
    for k, v := range values {
        k = strings.ToLower(k)
        m[k] = append(m[k], v...)
    }
    for _, v := range m {
        sort.Strings(v)
    }
    return m
}

// Encode the URI params as required by the API.  They are lower-cased,
// sorted and formatted as param:value,value,...\nparam:value...
func encodeParams(values map[string][]string) string {
    var keys []string
    values = toLowerKeys(values)
    for k := range values {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    var result []string
    for _, v := range keys {
        result = append(result, fmt.Sprintf("%v:%s", v, strings.Join(values[v], ",")))
    }
    return strings.Join(result, "\n")
}

// Calculate the headers required in the string to sign.
func composeHeaders(req *http.Request) string {
    var result []string
    for _, header_name := range headers_to_sign {
        result = append(result, req.Header.Get(header_name)+"\n")
    }
    return strings.Join(result, "")
}

// Calculate the x-ms-* headers, encode as for encodeParams.
func composeCanonicalizedHeaders(req *http.Request) string {
    var results []string
    for header_name, values := range req.Header {
        header_name = strings.ToLower(header_name)
        if strings.HasPrefix(header_name, "x-ms-") {
            results = append(results, fmt.Sprintf("%v:%s\n", header_name, strings.Join(values, ",")))
        }
    }
    sort.Strings(results)
    return strings.Join(results, "")
}

// Calculate the URI params and encode them in the string.
// See http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx
// for details of this encoding.
func composeCanonicalizedResource(req *http.Request, account_name string) string {
    path := req.URL.Path
    if !strings.HasPrefix(path, "/") {
        path = "/" + path
    }

    values := req.URL.Query()
    values_lower := toLowerKeys(values)
    param_string := encodeParams(values_lower)

    result := "/" + account_name + path
    if param_string != "" {
        result += "\n" + param_string
    }

    return result
}

// Take the passed ms_version string and add it to the request headers.
func addVersionHeader(req *http.Request, ms_version string) {
    req.Header.Set("x-ms-version", ms_version)
}

// Calculate the mD5sum and content length for the request payload and add
// as the Content-MD5 header and Content-Length header respectively.
func addContentHeaders(req *http.Request) {
    if req.Body == nil {
        if req.Method == "PUT" || req.Method == "POST" {
            // This cannot be set for a GET, likewise it *must* be set for
            // PUT and POST.
            req.Header.Set("Content-Length", "0")
        }
        return
    }
    reqdata, err := ioutil.ReadAll(req.Body)
    if err != nil {
        panic(fmt.Errorf("Unable to read request body: %s", err))
    }
    // Replace the request's data because we just destroyed it by reading it.
    req.Body = ioutil.NopCloser(bytes.NewReader(reqdata))
    req.Header.Set("Content-Length", fmt.Sprintf("%d", len(reqdata)))
    // Stop Go's http lib from chunking the data because Azure will return
    // an authorization error if it's chunked.
    req.ContentLength = int64(len(reqdata))
}

// Add a Date: header in RFC1123 format.
func addDateHeader(req *http.Request) {
    now := time.Now().UTC().Format(time.RFC1123)
    // The Azure API requires "GMT" and not "UTC".
    now = strings.Replace(now, "UTC", "GMT", 1)
    req.Header.Set("Date", now)
}

// Convenience function to add mandatory headers required in every request.
func addStandardHeaders(req *http.Request, account, key, ms_version string) {
    addVersionHeader(req, ms_version)
    addDateHeader(req)
    addContentHeaders(req)
    signRequest(req, account, key)
}

// StorageContext keeps track of the mandatory parameters required to send a
// request to the storage services API.  It also has an http Client so that
// the client is only created once for all requests.
type StorageContext struct {
    Account string
    Key     string
    client  *http.Client
}

// getClient is used when sending a request.  If called with an existing client
// it will replace the once in the context structure which is useful for tests.
func (context *StorageContext) getClient() *http.Client {
    if context.client == nil {
        return http.DefaultClient
    }
    return context.client
}

// Any object that deserializes XML must meet this interface.
type Deserializer interface {
    Deserialize([]byte) error
}

// Send a request to the storage service and process the response.
// The "res" parameter is typically an XML struct that will deserialize the
// raw XML into the struct data.  The http Response object is returned.
//
// If the response's HTTP status code is not the same as "expected_status"
// then an HTTPError will be returned as the error.  When the returned error
// is an HTTPError, the request response is also returned.  In other error
// cases, the returned response may be the one received from the server or
// it may be nil.
func (context *StorageContext) send(req *http.Request, res Deserializer, expected_status int) (*http.Response, error) {
    client := context.getClient()
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    var data []byte

    if resp.StatusCode != expected_status {
        if resp.Body != nil {
            data, err = ioutil.ReadAll(resp.Body)
            if err != nil {
                return resp, err
            }
        }
        msg := newHTTPError(resp.StatusCode, data, "Azure request failed")
        return resp, msg
    }

    // If the caller didn't supply an object to deserialize the message into
    // then just return.
    if res == nil {
        return resp, nil
    }

    // TODO: deserialize response headers into the "res" object.
    data, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        msg := fmt.Errorf("failed to read response data: %s", err)
        return resp, msg
    }
    err = res.Deserialize(data)
    if err != nil {
        msg := fmt.Errorf("Failed to deserialize data: %s", err)
        return resp, msg
    }

    return resp, nil
}

// getListContainersBatch calls the "List Containers" operation on the storage
// API, and returns a single batch of results; its "next marker" for batching,
// and an error code.
// The marker argument should be empty for a new List Containers request.  for
// subsequent calls to get additional batches of the same result, pass the
// "next marker" returned by the previous call.
// The "next marker" will be empty on the last batch.
func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, string, error) {
    uri := interpolateURL(
        "http://___.blob.core.windows.net/?comp=list", context.Account)
    req, err := http.NewRequest("GET", uri, nil)
    if err != nil {
        return nil, "", err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    containers := ContainerEnumerationResults{}
    _, err = context.send(req, &containers, http.StatusOK)
    if err != nil {
        return nil, "", err
    }

    // The response may contain a NextMarker field, to let us request a
    // subsequent batch of results.  The XML parser won't trim whitespace out
    // of the marker tag, so we do that here.
    nextMarker := strings.TrimSpace(containers.NextMarker)
    return &containers, nextMarker, nil
}

// ListContainers sends a request to the storage service to list the containers
// in the storage account.  error is non-nil if an error occurred.
func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) {
    containers := make([]Container, 0)
    var batch *ContainerEnumerationResults

    // Request the initial result, using the empty marker.  Then, for as long
    // as the result has a nonempty NextMarker, request the next batch using
    // that marker.
    for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker {
        var err error
        // Don't use := here or you'll shadow variables from the function's
        // outer scopes.
        batch, nextMarker, err = context.getListContainersBatch(marker)
        if err != nil {
            return nil, err
        }
        containers = append(containers, batch.Containers...)
    }

    // There's more in a ContainerEnumerationResults than just the containers.
    // Return the latest batch, but give it the full cumulative containers list
    // instead of just the last batch.
    // To the caller, this will look like they made one call to Azure's
    // List Containers method, but batch size was unlimited.
    batch.Containers = containers
    return batch, nil
}

// Send a request to the storage service to list the blobs in a container.
func (context *StorageContext) ListBlobs(container string) (*BlobEnumerationResults, error) {
    uri := interpolateURL(
        "http://___.blob.core.windows.net/___?restype=container&comp=list",
        context.Account, container)
    req, err := http.NewRequest("GET", uri, nil)
    if err != nil {
        return nil, err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    blob := &BlobEnumerationResults{}
    _, err = context.send(req, blob, http.StatusOK)
    return blob, err
}

// Send a request to the storage service to create a new container.  If the
// request fails, error is non-nil.
func (context *StorageContext) CreateContainer(container string) error {
    uri := interpolateURL(
        "http://___.blob.core.windows.net/___?restype=container",
        context.Account, container)
    req, err := http.NewRequest("PUT", uri, nil)
    if err != nil {
        return err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    _, errmsg := context.send(req, nil, http.StatusCreated)
    if errmsg != nil {
        return errmsg
    }
    return nil
}

// Send a request to create a space to upload a blob.  Note that this does not
// do the uploading, it just makes an empty file.
func (context *StorageContext) PutBlob(container, filename string) error {
    // TODO: Add blobtype header.
    uri := interpolateURL(
        "http://___.blob.core.windows.net/___/___",
        context.Account, container, filename)
    req, err := http.NewRequest("PUT", uri, nil)
    if err != nil {
        return err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    _, errmsg := context.send(req, nil, http.StatusCreated)
    if errmsg != nil {
        return errmsg
    }
    return nil
}

// Send a request to fetch the list of blocks that have been uploaded as part
// of a block blob.
func (context *StorageContext) GetBlockList(container, filename string) (*GetBlockList, error) {
    uri := interpolateURL(
        "http://___.blob.core.windows.net/___/___?comp=blocklist&blocklisttype=all",
        context.Account, container, filename)
    req, err := http.NewRequest("GET", uri, nil)
    if err != nil {
        return nil, err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    resp, errmsg := context.send(req, nil, http.StatusOK)
    if errmsg != nil {
        return nil, errmsg
    }
    bl := &GetBlockList{}
    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("Failed to read response data: %s", err)
    }

    err = bl.Deserialize(data)
    if err != nil {
        return nil, fmt.Errorf("Failed to deserialize data: %s", err)
    }

    return bl, nil
}

// Send a request to create a new block.  The request payload contains the
// data block to upload.
func (context *StorageContext) PutBlock(container, filename, id string, data io.Reader) error {
    base64_id := base64.StdEncoding.EncodeToString([]byte(id))
    uri := interpolateURL(
        "http://___.blob.core.windows.net/___/___?comp=block&blockid=___",
        context.Account, container, filename, base64_id)
    req, err := http.NewRequest("PUT", uri, data)
    if err != nil {
        return err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    resp, err := context.send(req, nil, http.StatusCreated)
    if err != nil {
        return err
    }
    if resp.StatusCode != http.StatusCreated {
        return fmt.Errorf("failed to put block: %s", resp.Status)
    }
    return nil
}

// Send a request to piece together blocks into a list that specifies a blob.
func (context *StorageContext) PutBlockList(container, filename string, blocklist *BlockList) error {
    uri := interpolateURL(
        "http://___.blob.core.windows.net/___/___?comp=blocklist",
        context.Account, container, filename)
    data, err := blocklist.Serialize()
    if err != nil {
        return err
    }
    data_reader := bytes.NewReader(data)
    req, err := http.NewRequest("PUT", uri, data_reader)
    if err != nil {
        return err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    resp, err := context.send(req, nil, http.StatusCreated)
    if err != nil {
        return err
    }
    if resp.StatusCode != http.StatusCreated {
        return fmt.Errorf("failed to put blocklist: %s", resp.Status)
    }
    return nil
}

// Delete the specified blob from the given container.
func (context *StorageContext) DeleteBlob(container, filename string) error {
    uri := interpolateURL(
        "http://___.blob.core.windows.net/___/___",
        context.Account, container, filename)
    req, err := http.NewRequest("DELETE", uri, nil)
    if err != nil {
        return err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    resp, err := context.send(req, nil, http.StatusAccepted)
    // TODO Handle a 404 with an <Error>BlobNotFound response body silently.
    if err != nil {
        return err
    }
    if resp.StatusCode != http.StatusAccepted {
        return fmt.Errorf("failed to delete blob: %s", resp.Status)
    }
    return nil
}

// Get the specified blob from the given container.
func (context *StorageContext) GetBlob(container, filename string) (io.ReadCloser, error) {
    uri := interpolateURL(
        "http://___.blob.core.windows.net/___/___",
        context.Account, container, filename)
    req, err := http.NewRequest("GET", uri, nil)
    if err != nil {
        return nil, err
    }
    addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
    resp, errmsg := context.send(req, nil, http.StatusOK)
    if errmsg != nil {
        return nil, errmsg
    }
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("failed to get blob: %s", resp.Status)
    }
    return resp.Body, nil
}