Skip to content

Commit

Permalink
add CA certificate verification and insecure option
Browse files Browse the repository at this point in the history
Signed-off-by: Jan-Otto Kröpke <[email protected]>
  • Loading branch information
jkroepke committed Sep 9, 2022
1 parent 58e0baa commit fa3edc8
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 41 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

ENHANCEMENTS:

* data-source/http: Added `ca_cert_pem` attribute which allows PEM encoded certificate(s) to be included in the set of root certificate authorities used when verifying server certificates ([#125](https://github.com/hashicorp/terraform-provider-http/pull/125)).
* data-source/http: Added `insecure` attribute to allow disabling of the verification of a server's certificate chain and host name. Defaults to `false` ([#125](https://github.com/hashicorp/terraform-provider-http/pull/125)).
* data-source/http: Allow optionally specifying HTTP request method and body ([#21](https://github.com/hashicorp/terraform-provider-http/issues/21)).

## 3.0.1 (July 27, 2022)
Expand Down
2 changes: 2 additions & 0 deletions docs/data-sources/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ resource "null_resource" "example" {

### Optional

- `ca_cert_pem` (String) Certificate data of the Certificate Authority (CA) in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
- `insecure` (Boolean) Disables verification of the server's certificate chain and hostname. Defaults to `false`
- `method` (String) The HTTP Method for the request. Allowed methods are a subset of methods defined in [RFC7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3) namely, `GET`, `HEAD`, and `POST`. `POST` support is only intended for read-only URLs, such as submitting a search.
- `request_body` (String) The request body as a string.
- `request_headers` (Map of String) A map of request header field names and values.
Expand Down
45 changes: 44 additions & 1 deletion internal/provider/data_source_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package provider

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"mime"
Expand Down Expand Up @@ -93,6 +95,19 @@ your control should be treated as untrustworthy.`,
DeprecationMessage: "Use response_body instead",
},

"ca_cert_pem": {
Description: "Certificate data of the Certificate Authority (CA) " +
"in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.",
Type: types.StringType,
Optional: true,
},

"insecure": {
Description: "Disables verification of the server's certificate chain and hostname. Defaults to `false`",
Type: types.BoolType,
Optional: true,
},

"response_headers": {
Description: `A map of response header field names and values.` +
` Duplicate headers are concatenated according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).`,
Expand Down Expand Up @@ -136,7 +151,33 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
method = "GET"
}

client := &http.Client{}
caCertificate := model.CaCertificate

tr := &http.Transport{
TLSClientConfig: &tls.Config{},
}

if !model.Insecure.IsNull() {
tr.TLSClientConfig.InsecureSkipVerify = model.Insecure.Value
}

// Use `ca_cert_pem` cert pool
if !caCertificate.IsNull() {
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM([]byte(caCertificate.Value)); !ok {
resp.Diagnostics.AddError(
"Error tls",
"Error tls: Can't add the CA certificate to certificate pool. Only PEM encoded certificates are supported.",
)
return
}

tr.TLSClientConfig.RootCAs = caCertPool
}

client := &http.Client{
Transport: tr,
}

request, err := http.NewRequestWithContext(ctx, method, url, requestBody)
if err != nil {
Expand Down Expand Up @@ -246,6 +287,8 @@ type modelV0 struct {
RequestHeaders types.Map `tfsdk:"request_headers"`
RequestBody types.String `tfsdk:"request_body"`
ResponseHeaders types.Map `tfsdk:"response_headers"`
CaCertificate types.String `tfsdk:"ca_cert_pem"`
Insecure types.Bool `tfsdk:"insecure"`
ResponseBody types.String `tfsdk:"response_body"`
Body types.String `tfsdk:"body"`
StatusCode types.Int64 `tfsdk:"status_code"`
Expand Down
213 changes: 173 additions & 40 deletions internal/provider/data_source_http_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package provider

import (
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand Down Expand Up @@ -344,57 +347,187 @@ func TestDataSource_UnsupportedMethod(t *testing.T) {
})
}

func TestDataSource_WithCACertificate(t *testing.T) {
t.Parallel()
svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer svr.Close()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
data "http" "http_test" {
url = "%s"
ca_cert_pem = <<EOF
%s
EOF
}`, svr.URL, CertToPEM(svr.Certificate())),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"),
),
},
},
})
}

func TestDataSource_WithCACertificateFalse(t *testing.T) {
t.Parallel()
svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer svr.Close()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
data "http" "http_test" {
url = "%s"
ca_cert_pem = "invalid"
}`, svr.URL),
ExpectError: regexp.MustCompile(`Can't add the CA certificate to certificate pool. Only PEM encoded\ncertificates are supported.`),
},
},
})
}

func TestDataSource_InsecureTrue(t *testing.T) {
t.Parallel()
svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer svr.Close()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
data "http" "http_test" {
url = "%s"
insecure = true
}`, svr.URL),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"),
),
},
},
})
}

func TestDataSource_InsecureFalse(t *testing.T) {
t.Parallel()
svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer svr.Close()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
data "http" "http_test" {
url = "%s"
insecure = false
}`, svr.URL),
ExpectError: regexp.MustCompile(fmt.Sprintf(`Error making request: Get "%s": x509: `, svr.URL)),
},
},
})
}

func TestDataSource_InsecureUnconfigured(t *testing.T) {
t.Parallel()
svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer svr.Close()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
data "http" "http_test" {
url = "%s"
}`, svr.URL),
ExpectError: regexp.MustCompile(fmt.Sprintf(`Error making request: Get "%s": x509: `, svr.URL)),
},
},
})
}

type TestHttpMock struct {
server *httptest.Server
}

func setUpMockHttpServer() *TestHttpMock {
Server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Header().Add("X-Single", "foobar")
w.Header().Add("X-Double", "1")
w.Header().Add("X-Double", "2")

switch r.URL.Path {
case "/200":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
case "/restricted":
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
} else {
w.WriteHeader(http.StatusForbidden)
}
case "/utf-8/200":
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
case "/utf-16/200":
w.Header().Set("Content-Type", "application/json; charset=UTF-16")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
case "/x509-ca-cert/200":
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("pem"))
case "/create":
if r.Method == "POST" {
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte("created"))
}
case "/head":
if r.Method == "HEAD" {
w.WriteHeader(http.StatusOK)
}
default:
w.WriteHeader(http.StatusNotFound)
}
httpReqHandler(w, r)
}),
)

return &TestHttpMock{
server: Server,
}
}

func httpReqHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Header().Add("X-Single", "foobar")
w.Header().Add("X-Double", "1")
w.Header().Add("X-Double", "2")

switch r.URL.Path {
case "/200":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
case "/restricted":
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
} else {
w.WriteHeader(http.StatusForbidden)
}
case "/utf-8/200":
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
case "/utf-16/200":
w.Header().Set("Content-Type", "application/json; charset=UTF-16")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
case "/x509-ca-cert/200":
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("pem"))
case "/create":
if r.Method == "POST" {
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte("created"))
}
case "/head":
if r.Method == "HEAD" {
w.WriteHeader(http.StatusOK)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}

// CertToPEM is a utility function returns a PEM encoded x509 Certificate.
func CertToPEM(cert *x509.Certificate) string {
certPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))

return strings.Trim(certPem, "\n")
}

0 comments on commit fa3edc8

Please sign in to comment.