github.com/hashicorp/packer@v1.14.3/datasource/http/data.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 //go:generate packer-sdc struct-markdown 5 //go:generate packer-sdc mapstructure-to-hcl2 -type DatasourceOutput,Config 6 package http 7 8 import ( 9 "context" 10 "fmt" 11 "io" 12 "mime" 13 "net/http" 14 "regexp" 15 "strings" 16 17 "github.com/hashicorp/hcl/v2/hcldec" 18 "github.com/hashicorp/packer-plugin-sdk/common" 19 "github.com/hashicorp/packer-plugin-sdk/hcl2helper" 20 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 21 "github.com/hashicorp/packer-plugin-sdk/template/config" 22 "github.com/zclconf/go-cty/cty" 23 ) 24 25 type Config struct { 26 common.PackerConfig `mapstructure:",squash"` 27 // The URL to request data from. This URL must respond with a `2xx` range response code and a `text/*` or `application/json` Content-Type. 28 Url string `mapstructure:"url" required:"true"` 29 // HTTP method used for the request. Supported methods are `HEAD`, `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `PATCH`. Default is `GET`. 30 Method string `mapstructure:"method" required:"false"` 31 // A map of strings representing additional HTTP headers to include in the request. 32 RequestHeaders map[string]string `mapstructure:"request_headers" required:"false"` 33 // HTTP request payload send with the request. Default is empty. 34 RequestBody string `mapstructure:"request_body" required:"false"` 35 } 36 37 type Datasource struct { 38 config Config 39 } 40 41 type DatasourceOutput struct { 42 // The URL the data was requested from. 43 Url string `mapstructure:"url"` 44 // The raw body of the HTTP response. 45 ResponseBody string `mapstructure:"body"` 46 // A map of strings representing the response HTTP headers. 47 // Duplicate headers are concatenated with, according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2). 48 ResponseHeaders map[string]string `mapstructure:"request_headers"` 49 } 50 51 func (d *Datasource) ConfigSpec() hcldec.ObjectSpec { 52 return d.config.FlatMapstructure().HCL2Spec() 53 } 54 55 func (d *Datasource) Configure(raws ...interface{}) error { 56 err := config.Decode(&d.config, nil, raws...) 57 if err != nil { 58 return err 59 } 60 61 var errs *packersdk.MultiError 62 63 if d.config.Url == "" { 64 errs = packersdk.MultiErrorAppend( 65 errs, 66 fmt.Errorf("the `url` must be specified")) 67 } 68 69 // Default to GET if no method is specified 70 if d.config.Method == "" { 71 d.config.Method = "GET" 72 } 73 74 // Check if the input is in the list of allowed methods 75 validMethod := false 76 allowedMethods := []string{"HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"} 77 for _, method := range allowedMethods { 78 if method == d.config.Method { 79 validMethod = true 80 break 81 } 82 } 83 if !validMethod { 84 errs = packersdk.MultiErrorAppend( 85 errs, 86 fmt.Errorf("the `method` must be one of %v", allowedMethods)) 87 } 88 89 if errs != nil && len(errs.Errors) > 0 { 90 return errs 91 } 92 return nil 93 } 94 95 func (d *Datasource) OutputSpec() hcldec.ObjectSpec { 96 return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec() 97 } 98 99 // This is to prevent potential issues w/ binary files 100 // and generally unprintable characters 101 // See https://github.com/hashicorp/terraform/pull/3858#issuecomment-156856738 102 func isContentTypeText(contentType string) bool { 103 104 parsedType, params, err := mime.ParseMediaType(contentType) 105 if err != nil { 106 return false 107 } 108 109 allowedContentTypes := []*regexp.Regexp{ 110 regexp.MustCompile("^text/.+"), 111 regexp.MustCompile("^application/json$"), 112 regexp.MustCompile(`^application/samlmetadata\+xml`), 113 } 114 115 for _, r := range allowedContentTypes { 116 if r.MatchString(parsedType) { 117 charset := strings.ToLower(params["charset"]) 118 return charset == "" || charset == "utf-8" || charset == "us-ascii" 119 } 120 } 121 122 return false 123 } 124 125 // Most of this code comes from http terraform provider data source 126 // https://github.com/hashicorp/terraform-provider-http/blob/main/internal/provider/data_source.go 127 func (d *Datasource) Execute() (cty.Value, error) { 128 ctx := context.TODO() 129 url, method, headers := d.config.Url, d.config.Method, d.config.RequestHeaders 130 client := &http.Client{} 131 132 // Create request body if it is provided 133 var requestBody io.Reader 134 if d.config.RequestBody != "" { 135 requestBody = strings.NewReader(d.config.RequestBody) 136 } 137 138 req, err := http.NewRequestWithContext(ctx, method, url, requestBody) 139 // TODO: How to make a test case for this? 140 if err != nil { 141 fmt.Println("Error creating http request") 142 return cty.NullVal(cty.EmptyObject), err 143 } 144 145 for name, value := range headers { 146 req.Header.Set(name, value) 147 } 148 149 resp, err := client.Do(req) 150 // TODO: How to make test case for this 151 if err != nil { 152 fmt.Println("Error making performing http request") 153 return cty.NullVal(cty.EmptyObject), err 154 } 155 156 defer resp.Body.Close() 157 158 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 159 return cty.NullVal(cty.EmptyObject), fmt.Errorf("HTTP request error. Response code: %d", resp.StatusCode) 160 } 161 162 contentType := resp.Header.Get("Content-Type") 163 if contentType == "" || isContentTypeText(contentType) == false { 164 fmt.Printf("Content-Type is not recognized as a text type, got %q\n", 165 contentType) 166 fmt.Println("If the content is binary data, Packer may not properly handle the contents of the response.") 167 } 168 169 bytes, err := io.ReadAll(resp.Body) 170 // TODO: How to make test case for this? 171 if err != nil { 172 fmt.Println("Error processing response body of call") 173 return cty.NullVal(cty.EmptyObject), err 174 } 175 176 responseHeaders := make(map[string]string) 177 for k, v := range resp.Header { 178 // Concatenate according to RFC2616 179 // cf. https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 180 responseHeaders[k] = strings.Join(v, ", ") 181 } 182 183 output := DatasourceOutput{ 184 Url: d.config.Url, 185 ResponseHeaders: responseHeaders, 186 ResponseBody: string(bytes), 187 } 188 return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil 189 }