Skip to content

Commit

Permalink
add CA certificate verification and insecure option
Browse files Browse the repository at this point in the history
  • Loading branch information
jkroepke committed Jun 14, 2022
1 parent 21ddb8a commit 3df1515
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 32 deletions.
2 changes: 2 additions & 0 deletions docs/data-sources/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ data "http" "example" {

### Optional

- `ca_certificate` - (String) PEM-encoded root certificates bundle for TLS authentication.
- `insecure` - (Boolean) Whether server should be accessed without verifying the TLS certificate. Defaults to false.
- `request_headers` (Map of String) A map of request header field names and values.

### Read-Only
Expand Down
38 changes: 37 additions & 1 deletion internal/provider/data_source.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 @@ -69,15 +71,49 @@ your control should be treated as untrustworthy.`,
Type: schema.TypeString,
},
},
"ca_certificate": {
Type: schema.TypeString,
Optional: true,
},

"insecure": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
}
}

func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) {
url := d.Get("url").(string)
headers := d.Get("request_headers").(map[string]interface{})
caCert := d.Get("ca_certificate").(string)

client := &http.Client{}
// Get the System Cert Pool
caCertPool, err := x509.SystemCertPool()
if err != nil {
return append(diags, diag.Errorf("Error tls: %s", err)...)
}

// Use `ca_certificate` cert pool
if caCert != "" {
caCertPool = x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM([]byte(caCert)); !ok {
return append(diags, diag.Errorf("Error tls: Can't add the CA certificate to certificate pool")...)
}
}

tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
InsecureSkipVerify: d.Get("insecure").(bool),
},
}

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

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
Expand Down
258 changes: 227 additions & 31 deletions internal/provider/data_source_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package provider

import (
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand Down Expand Up @@ -148,6 +152,176 @@ func TestDataSource_utf16(t *testing.T) {
// },
// })
// }
const testDatasourceConfigBasicTlsInsecure = `
data "http" "http_test" {
url = "%s/meta_%d.txt"
insecure = true
}
output "body" {
value = data.http.http_test.body
}
output "response_headers" {
value = data.http.http_test.response_headers
}
`

func TestDataSource_http200_TLS_insecure(t *testing.T) {
testHttpMock := setUpMockHttpTLSServer()

defer testHttpMock.server.Close()

resource.UnitTest(t, resource.TestCase{
ProviderFactories: testProviders(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDatasourceConfigBasicTlsInsecure, testHttpMock.server.URL, 200),
Check: func(s *terraform.State) error {
_, ok := s.RootModule().Resources["data.http.http_test"]
if !ok {
return fmt.Errorf("missing data resource")
}

outputs := s.RootModule().Outputs

if outputs["body"].Value != "1.0.0" {
return fmt.Errorf(
`'body' output is %s; want '1.0.0'`,
outputs["body"].Value,
)
}

responseHeaders := outputs["response_headers"].Value.(map[string]interface{})

if responseHeaders["X-Single"].(string) != "foobar" {
return fmt.Errorf(
`'X-Single' response header is %s; want 'foobar'`,
responseHeaders["X-Single"].(string),
)
}

if responseHeaders["X-Double"].(string) != "1, 2" {
return fmt.Errorf(
`'X-Double' response header is %s; want '1, 2'`,
responseHeaders["X-Double"].(string),
)
}

return nil
},
},
},
})
}

const testDataSourceConfigBasicTLSCA = `
data "http" "http_test" {
url = "%s/meta_%d.txt"
ca_certificate = <<EOF
%s
EOF
}
output "body" {
value = data.http.http_test.body
}
output "response_headers" {
value = data.http.http_test.response_headers
}
`

func TestDataSource_http200_TLS_CA(t *testing.T) {
testHttpMock := setUpMockHttpTLSServer()

defer testHttpMock.server.Close()

resource.UnitTest(t, resource.TestCase{
ProviderFactories: testProviders(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDataSourceConfigBasicTLSCA, testHttpMock.server.URL, 200, CertToPEM(testHttpMock.server.Certificate())),
Check: func(s *terraform.State) error {
_, ok := s.RootModule().Resources["data.http.http_test"]
if !ok {
return fmt.Errorf("missing data resource")
}

outputs := s.RootModule().Outputs

if outputs["body"].Value != "1.0.0" {
return fmt.Errorf(
`'body' output is %s; want '1.0.0'`,
outputs["body"].Value,
)
}

responseHeaders := outputs["response_headers"].Value.(map[string]interface{})

if responseHeaders["X-Single"].(string) != "foobar" {
return fmt.Errorf(
`'X-Single' response header is %s; want 'foobar'`,
responseHeaders["X-Single"].(string),
)
}

if responseHeaders["X-Double"].(string) != "1, 2" {
return fmt.Errorf(
`'X-Double' response header is %s; want '1, 2'`,
responseHeaders["X-Double"].(string),
)
}

return nil
},
},
},
})
}

const testDataSourceConfigUTF8TLSInsecure = `
data "http" "http_test" {
url = "%s/utf-8/meta_%d.txt"
insecure = true
}
output "body" {
value = "${data.http.http_test.body}"
}
`

func TestDataSource_utf8_TLS_insecure(t *testing.T) {
testHttpMock := setUpMockHttpTLSServer()

defer testHttpMock.server.Close()

resource.UnitTest(t, resource.TestCase{
ProviderFactories: testProviders(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDataSourceConfigUTF8TLSInsecure, testHttpMock.server.URL, 200),
Check: func(s *terraform.State) error {
_, ok := s.RootModule().Resources["data.http.http_test"]
if !ok {
return fmt.Errorf("missing data resource")
}

outputs := s.RootModule().Outputs

if outputs["body"].Value != "1.0.0" {
return fmt.Errorf(
`'body' output is %s; want '1.0.0'`,
outputs["body"].Value,
)
}

return nil
},
},
},
})
}

const testDataSourceConfigBasic = `
data "http" "http_test" {
Expand Down Expand Up @@ -184,42 +358,64 @@ type TestHttpMock struct {
func setUpMockHttpServer() *TestHttpMock {
Server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpReqHandler(w, r)
}),
)

return &TestHttpMock{
server: Server,
}
}

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")
if r.URL.Path == "/meta_200.txt" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
} else if r.URL.Path == "/restricted/meta_200.txt" {
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
} else {
w.WriteHeader(http.StatusForbidden)
}
} else if r.URL.Path == "/utf-8/meta_200.txt" {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
} else if r.URL.Path == "/utf-16/meta_200.txt" {
w.Header().Set("Content-Type", "application/json; charset=UTF-16")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("\"1.0.0\""))
} else if r.URL.Path == "/x509/cert.pem" {
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("pem"))
} else if r.URL.Path == "/meta_404.txt" {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusNotFound)
}
func setUpMockHttpTLSServer() *TestHttpMock {
Server := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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")
if r.URL.Path == "/meta_200.txt" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
} else if r.URL.Path == "/restricted/meta_200.txt" {
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
} else {
w.WriteHeader(http.StatusForbidden)
}
} else if r.URL.Path == "/utf-8/meta_200.txt" {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("1.0.0"))
} else if r.URL.Path == "/utf-16/meta_200.txt" {
w.Header().Set("Content-Type", "application/json; charset=UTF-16")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("\"1.0.0\""))
} else if r.URL.Path == "/x509/cert.pem" {
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("pem"))
} else if r.URL.Path == "/meta_404.txt" {
w.WriteHeader(http.StatusNotFound)
} else {
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 3df1515

Please sign in to comment.