Skip to content
This repository has been archived by the owner on Aug 6, 2023. It is now read-only.

Commit

Permalink
feat: Add server certificate validation with Root CA and insecure opt…
Browse files Browse the repository at this point in the history
…ion (#1)

* add CA certificate verification and insecure option

* fix fmt

* don't try to convert bool error in string

* add tests

* use system CA or ca_certificate, but not both

* fix diag messages
  • Loading branch information
barryib authored May 6, 2021
1 parent e1ae2be commit 4e8da32
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 32 deletions.
4 changes: 4 additions & 0 deletions docs/data-sources/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ The following arguments are supported:
* `request_headers` - (Optional) A map of strings representing additional HTTP
headers to include in the request.

* `ca_certificate` - (Optional) PEM-encoded root certificates bundle for TLS authentication.

* `insecure` - (Optional) Whether server should be accessed without verifying the TLS certificate. Defaults to false.

## Attributes Reference

The following attributes are exported:
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 @@ -49,15 +51,49 @@ func dataSource() *schema.Resource {
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)

// 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")...)
}
}

client := &http.Client{}
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
257 changes: 226 additions & 31 deletions internal/provider/data_source_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 @@ -254,46 +257,238 @@ func TestDataSource_utf16(t *testing.T) {
// },
// })
// }
const testDataSourceConfig_basic_TLS_insecure = `
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{
Providers: testProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDataSourceConfig_basic_TLS_insecure, 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,
)
}

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

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

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

return nil
},
},
},
})
}

const testDataSourceConfig_basic_TLS_CA = `
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{
Providers: testProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDataSourceConfig_basic_TLS_CA, 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,
)
}

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

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

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

return nil
},
},
},
})
}

const testDataSourceConfig_utf8_TLS_insecure = `
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{
Providers: testProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testDataSourceConfig_utf8_TLS_insecure, 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
},
},
},
})
}

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 4e8da32

Please sign in to comment.