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  }