From e6c11a53dd0d584f2d610047490e7784cf612c18 Mon Sep 17 00:00:00 2001 From: Matt Morrison Date: Mon, 5 Sep 2016 07:58:12 +1200 Subject: [PATCH] Add support for content_type, headers + md5_digest --- .../data_source_storage_object_signed_url.go | 96 +++++---- ...a_source_storage_object_signed_url_test.go | 183 +++++++++++++++++- 2 files changed, 242 insertions(+), 37 deletions(-) diff --git a/builtin/providers/google/data_source_storage_object_signed_url.go b/builtin/providers/google/data_source_storage_object_signed_url.go index 828e9ec07..9149d45f8 100644 --- a/builtin/providers/google/data_source_storage_object_signed_url.go +++ b/builtin/providers/google/data_source_storage_object_signed_url.go @@ -10,16 +10,18 @@ import ( "encoding/base64" "encoding/pem" "fmt" - "github.com/hashicorp/terraform/helper/pathorcontents" - "github.com/hashicorp/terraform/helper/schema" - "golang.org/x/oauth2/google" - "golang.org/x/oauth2/jwt" "log" "net/url" "os" "strconv" "strings" "time" + + "github.com/hashicorp/terraform/helper/pathorcontents" + "github.com/hashicorp/terraform/helper/schema" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jwt" + "sort" ) const gcsBaseUrl = "https://storage.googleapis.com" @@ -34,12 +36,11 @@ func dataSourceGoogleSignedUrl() *schema.Resource { Type: schema.TypeString, Required: true, }, - //TODO: implement support - //"content_type": &schema.Schema{ - // Type: schema.TypeString, - // Optional: true, - // Default: "", - //}, + "content_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, "credentials": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -54,17 +55,16 @@ func dataSourceGoogleSignedUrl() *schema.Resource { Optional: true, Default: "GET", }, - //TODO: implement support - //"http_headers": &schema.Schema{ - // Type: schema.TypeList, - // Optional: true, - //}, - //TODO: implement support - //"md5_digest": &schema.Schema{ - // Type: schema.TypeString, - // Optional: true, - // Default: "", - //}, + "http_headers": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Elem: schema.TypeString, + }, + "md5_digest": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, "path": &schema.Schema{ Type: schema.TypeString, Required: true, @@ -80,7 +80,6 @@ func dataSourceGoogleSignedUrl() *schema.Resource { func dataSourceGoogleSignedUrlRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Build UrlData object from data source attributes urlData := &UrlData{} @@ -103,6 +102,25 @@ func dataSourceGoogleSignedUrlRead(d *schema.ResourceData, meta interface{}) err expires := time.Now().Unix() + int64(duration.Seconds()) urlData.Expires = int(expires) + if v, ok := d.GetOk("content_type"); ok { + urlData.ContentType = v.(string) + } + + if v, ok := d.GetOk("http_headers"); ok { + hdrMap := v.(map[string]interface{}) + + if len(hdrMap) > 0 { + urlData.HttpHeaders = make(map[string]string, len(hdrMap)) + for k, v := range hdrMap { + urlData.HttpHeaders[k] = v.(string) + } + } + } + + if v, ok := d.GetOk("md5_digest"); ok { + urlData.Md5Digest = v.(string) + } + // object path path := []string{ "", @@ -112,7 +130,6 @@ func dataSourceGoogleSignedUrlRead(d *schema.ResourceData, meta interface{}) err objectPath := strings.Join(path, "/") urlData.Path = objectPath - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Load JWT Config from Google Credentials jwtConfig, err := loadJwtConfig(d, config) if err != nil { @@ -120,7 +137,6 @@ func dataSourceGoogleSignedUrlRead(d *schema.ResourceData, meta interface{}) err } urlData.JwtConfig = jwtConfig - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Sign url object data signature, err := SignString(urlData.CreateSigningString(), jwtConfig) if err != nil { @@ -128,7 +144,6 @@ func dataSourceGoogleSignedUrlRead(d *schema.ResourceData, meta interface{}) err } urlData.Signature = signature - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Construct URL finalUrl := urlData.BuildUrl() d.SetId(urlData.EncodedSignature()) @@ -204,11 +219,14 @@ func parsePrivateKey(key []byte) (*rsa.PrivateKey, error) { } type UrlData struct { - JwtConfig *jwt.Config - HttpMethod string - Expires int - Path string - Signature []byte + JwtConfig *jwt.Config + ContentType string + HttpMethod string + Expires int + Md5Digest string + HttpHeaders map[string]string + Path string + Signature []byte } // Creates a string in the form ready for signing: @@ -229,11 +247,11 @@ func (u *UrlData) CreateSigningString() []byte { buf.WriteString("\n") // MD5 digest (optional) - // TODO + buf.WriteString(u.Md5Digest) buf.WriteString("\n") // request content-type (optional) - // TODO + buf.WriteString(u.ContentType) buf.WriteString("\n") // signed url expiration @@ -241,11 +259,23 @@ func (u *UrlData) CreateSigningString() []byte { buf.WriteString("\n") // additional request headers (optional) - // TODO + // Must be sorted in lexigraphical order + var keys []string + for k := range u.HttpHeaders { + keys = append(keys, strings.ToLower(k)) + } + sort.Strings(keys) + + // To perform the opertion you want + for _, k := range keys { + buf.WriteString(fmt.Sprintf("%s:%s\n", k, u.HttpHeaders[k])) + } // object path buf.WriteString(u.Path) + fmt.Printf("SIGNING STRING: \n%s\n", buf.String()) + return buf.Bytes() } diff --git a/builtin/providers/google/data_source_storage_object_signed_url_test.go b/builtin/providers/google/data_source_storage_object_signed_url_test.go index d97a67e30..e4a78c032 100644 --- a/builtin/providers/google/data_source_storage_object_signed_url_test.go +++ b/builtin/providers/google/data_source_storage_object_signed_url_test.go @@ -12,7 +12,9 @@ import ( "github.com/hashicorp/terraform/terraform" "golang.org/x/oauth2/google" "io/ioutil" + "net/http" "net/url" + "strings" ) const fakeCredentials = `{ @@ -130,13 +132,92 @@ func TestDatasourceSignedUrl_accTest(t *testing.T) { resource.TestStep{ Config: testAccTestGoogleStorageObjectSingedUrl(bucketName), Check: resource.ComposeTestCheckFunc( - testAccGoogleSignedUrlRetrieval("data.google_storage_object_signed_url.story_url"), + testAccGoogleSignedUrlRetrieval("data.google_storage_object_signed_url.story_url", nil), ), }, }, }) } +func TestDatasourceSignedUrl_wHeaders(t *testing.T) { + + headers := map[string]string{ + "x-goog-test": "foo", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccTestGoogleStorageObjectSingedUrl_wHeader(), + Check: resource.ComposeTestCheckFunc( + testAccGoogleSignedUrlRetrieval("data.google_storage_object_signed_url.story_url_w_headers", headers), + ), + }, + }, + }) +} + +func TestDatasourceSignedUrl_wContentType(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccTestGoogleStorageObjectSingedUrl_wContentType(), + Check: resource.ComposeTestCheckFunc( + testAccGoogleSignedUrlRetrieval("data.google_storage_object_signed_url.story_url_w_content_type", nil), + ), + }, + }, + }) +} + +func TestDatasourceSignedUrl_wMD5(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccTestGoogleStorageObjectSingedUrl_wMD5(), + Check: resource.ComposeTestCheckFunc( + testAccGoogleSignedUrlRetrieval("data.google_storage_object_signed_url.story_url_w_md5", nil), + ), + }, + }, + }) +} + +// formatRequest generates ascii representation of a request +func formatRequest(r *http.Request) string { + // Create return string + var request []string + request = append(request, "--------") + // Add the request string + url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto) + request = append(request, url) + // Add the host + request = append(request, fmt.Sprintf("Host: %v", r.Host)) + // Loop through headers + for name, headers := range r.Header { + //name = strings.ToLower(name) + for _, h := range headers { + request = append(request, fmt.Sprintf("%v: %v", name, h)) + } + } + + // If this is a POST, add post data + if r.Method == "POST" { + r.ParseForm() + request = append(request, "\n") + request = append(request, r.Form.Encode()) + } + request = append(request, "--------") + // Return the request as a string + return strings.Join(request, "\n") +} + func testAccGoogleSignedUrlExists(n string) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -151,9 +232,12 @@ func testAccGoogleSignedUrlExists(n string) resource.TestCheckFunc { } } -func testAccGoogleSignedUrlRetrieval(n string) resource.TestCheckFunc { +func testAccGoogleSignedUrlRetrieval(n string, headers map[string]string) resource.TestCheckFunc { return func(s *terraform.State) error { r := s.RootModule().Resources[n] + if r == nil { + return fmt.Errorf("Datasource not found") + } a := r.Primary.Attributes if a["signed_url"] == "" { @@ -161,10 +245,38 @@ func testAccGoogleSignedUrlRetrieval(n string) resource.TestCheckFunc { } url := a["signed_url"] + fmt.Printf("URL: %s\n", url) + method := a["http_method"] + + req, _ := http.NewRequest(method, url, nil) + + // Apply custom headers to request + for k, v := range headers { + fmt.Printf("Adding Header (%s: %s)\n", k, v) + req.Header.Set(k, v) + } + + contentType := a["content_type"] + if contentType != "" { + fmt.Printf("Adding Content-Type: %s\n", contentType) + req.Header.Add("Content-Type", contentType) + } + + md5Digest := a["md5_digest"] + if md5Digest != "" { + fmt.Printf("Adding Content-MD5: %s\n", md5Digest) + req.Header.Add("Content-MD5", md5Digest) + } // send request to GET object using signed url client := cleanhttp.DefaultClient() - response, err := client.Get(url) + + // Print request + //dump, _ := httputil.DumpRequest(req, true) + //fmt.Printf("%+q\n", strings.Replace(string(dump), "\\n", "\n", 99)) + fmt.Printf("%s\n", formatRequest(req)) + + response, err := client.Do(req) if err != nil { return err } @@ -206,6 +318,69 @@ data "google_storage_object_signed_url" "story_url" { bucket = "${google_storage_bucket.bucket.name}" path = "${google_storage_bucket_object.story.name}" +}`, bucketName) } -`, bucketName) + +func testAccTestGoogleStorageObjectSingedUrl_wHeader() string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-signurltest-%s" +} + +resource "google_storage_bucket_object" "story" { + name = "path/to/file" + bucket = "${google_storage_bucket.bucket.name}" + + content = "once upon a time..." +} + +data "google_storage_object_signed_url" "story_url_w_headers" { + bucket = "${google_storage_bucket.bucket.name}" + path = "${google_storage_bucket_object.story.name}" + http_headers { + x-goog-test = "foo" + } +}`, acctest.RandString(6)) +} + +func testAccTestGoogleStorageObjectSingedUrl_wContentType() string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-signurltest-%s" +} + +resource "google_storage_bucket_object" "story" { + name = "path/to/file" + bucket = "${google_storage_bucket.bucket.name}" + + content = "once upon a time..." +} + +data "google_storage_object_signed_url" "story_url_w_content_type" { + bucket = "${google_storage_bucket.bucket.name}" + path = "${google_storage_bucket_object.story.name}" + + content_type = "text/plain" +}`, acctest.RandString(6)) +} + +func testAccTestGoogleStorageObjectSingedUrl_wMD5() string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-signurltest-%s" +} + +resource "google_storage_bucket_object" "story" { + name = "path/to/file" + bucket = "${google_storage_bucket.bucket.name}" + + content = "once upon a time..." +} + +data "google_storage_object_signed_url" "story_url_w_md5" { + bucket = "${google_storage_bucket.bucket.name}" + path = "${google_storage_bucket_object.story.name}" + + md5_digest = "${google_storage_bucket_object.story.md5hash}" +}`, acctest.RandString(6)) }