pkg/tool/http: add tls settings

tls.verify can be used to disable server certificate validation.
tls.caCert can be used to provide a PEM encoded certificates to validate
the server certificate.

Closes #1558

Signed-off-by: Jean-Philippe Braun <eon@patapon.info>
Change-Id: If8f0aa5d9f882675e84e2546faa510f7d3bcde1c
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/533845
Unity-Result: CUEcueckoo <cueckoo@cuelang.org>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Reviewed-by: Marcel van Lohuizen <mpvl@gmail.com>
diff --git a/cmd/cue/cmd/testdata/script/cmd_http.txt b/cmd/cue/cmd/testdata/script/cmd_http.txt
index 4c240b1..9ead053 100644
--- a/cmd/cue/cmd/testdata/script/cmd_http.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_http.txt
@@ -4,24 +4,25 @@
 {"data":"I'll be back!","when":"now"}
 
 -- task_tool.cue --
-
 package home
 
+import (
+	h "tool/http"
+	"tool/cli"
+)
+
 command: http: {
 	task: testserver: {
 		kind: "testserver"
 		url:  string
 	}
-	task: http: {
-		kind:   "http"
-		method: "POST"
+	task: http: h.Post & {
 		url:    task.testserver.url
 
 		request: body:  "I'll be back!"
 		response: body: string // TODO: allow this to be a struct, parsing the body.
 	}
-	task: print: {
-		kind: "print"
+	task: print: cli.Print & {
 		text: task.http.response.body
 	}
 }
diff --git a/internal/task/task.go b/internal/task/task.go
index e8e585f..135c56e 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -52,8 +52,6 @@
 	f := c.Obj.Lookup(field)
 	value, err := f.Int64()
 	if err != nil {
-		// TODO: use v for position for now, as f has not yet a
-		// position associated with it.
 		c.addErr(f, err, "invalid integer argument")
 		return 0
 	}
@@ -64,8 +62,6 @@
 	f := c.Obj.Lookup(field)
 	value, err := f.String()
 	if err != nil {
-		// TODO: use v for position for now, as f has not yet a
-		// position associated with it.
 		c.addErr(f, err, "invalid string argument")
 		return ""
 	}
@@ -82,6 +78,16 @@
 	return value
 }
 
+func (c *Context) BoolPath(path cue.Path) bool {
+	f := c.Obj.LookupPath(path)
+	value, err := f.Bool()
+	if err != nil {
+		c.addErr(f, err, "invalid bool argument")
+		return false
+	}
+	return value
+}
+
 func (c *Context) addErr(v cue.Value, wrap error, format string, args ...interface{}) {
 
 	err := &taskError{
diff --git a/pkg/tool/http/doc.go b/pkg/tool/http/doc.go
index 9941311..0884f0f 100644
--- a/pkg/tool/http/doc.go
+++ b/pkg/tool/http/doc.go
@@ -15,6 +15,14 @@
 //     	method: string
 //     	url:    string // TODO: make url.URL type
 //
+//     	tls: {
+//     		// Whether the server certificate must be validated.
+//     		verify: *true | bool
+//     		// PEM encoded certificate(s) to validate the server certificate.
+//     		// If not set the CA bundle of the system is used.
+//     		caCert?: bytes | string
+//     	}
+//
 //     	request: {
 //     		body?: bytes | string
 //     		header: [string]:  string | [...string]
diff --git a/pkg/tool/http/http.cue b/pkg/tool/http/http.cue
index 1526561..19bcd03 100644
--- a/pkg/tool/http/http.cue
+++ b/pkg/tool/http/http.cue
@@ -25,6 +25,14 @@
 	method: string
 	url:    string // TODO: make url.URL type
 
+	tls: {
+		// Whether the server certificate must be validated.
+		verify: *true | bool
+		// PEM encoded certificate(s) to validate the server certificate.
+		// If not set the CA bundle of the system is used.
+		caCert?: bytes | string
+	}
+
 	request: {
 		body?: bytes | string
 		header: [string]:  string | [...string]
diff --git a/pkg/tool/http/http.go b/pkg/tool/http/http.go
index e6651be..61bff51 100644
--- a/pkg/tool/http/http.go
+++ b/pkg/tool/http/http.go
@@ -19,11 +19,15 @@
 
 import (
 	"bytes"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
 	"io"
 	"io/ioutil"
 	"net/http"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/internal/task"
 )
 
@@ -43,8 +47,9 @@
 func (c *httpCmd) Run(ctx *task.Context) (res interface{}, err error) {
 	var header, trailer http.Header
 	var (
-		method = ctx.String("method")
-		u      = ctx.String("url")
+		method    = ctx.String("method")
+		u         = ctx.String("url")
+		tlsVerify = ctx.BoolPath(cue.ParsePath("tls.verify"))
 	)
 	var r io.Reader
 	if obj := ctx.Obj.Lookup("request"); obj.Exists() {
@@ -63,10 +68,50 @@
 			return nil, err
 		}
 	}
+
+	var caCert []byte
+	caCertValue := ctx.Obj.LookupPath(cue.ParsePath("tls.caCert"))
+	if caCertValue.Exists() {
+		caCert, err = caCertValue.Bytes()
+		if err != nil {
+			return nil, errors.Wrapf(err, caCertValue.Pos(), "invalid bytes value")
+		}
+	}
+
 	if ctx.Err != nil {
 		return nil, ctx.Err
 	}
 
+	transport := http.DefaultTransport.(*http.Transport).Clone()
+	transport.TLSClientConfig = &tls.Config{}
+
+	if !tlsVerify {
+		transport.TLSClientConfig.InsecureSkipVerify = true
+	}
+	if tlsVerify && len(caCert) > 0 {
+		pool := x509.NewCertPool()
+		for {
+			block, rest := pem.Decode(caCert)
+			if block == nil {
+				break
+			}
+			if block.Type == "PUBLIC KEY" {
+				c, err := x509.ParseCertificate(block.Bytes)
+				if err != nil {
+					return nil, errors.Wrapf(err, ctx.Obj.Pos(), "failed to parse caCert")
+				}
+				pool.AddCert(c)
+			}
+			caCert = rest
+		}
+		transport.TLSClientConfig.RootCAs = pool
+	}
+
+	client := &http.Client{
+		Transport: transport,
+		// TODO: timeout
+	}
+
 	req, err := http.NewRequest(method, u, r)
 	if err != nil {
 		return nil, err
@@ -74,10 +119,8 @@
 	req.Header = header
 	req.Trailer = trailer
 
-	// TODO:
-	//  - retry logic
-	//  - TLS certs
-	resp, err := http.DefaultClient.Do(req)
+	// TODO: retry logic
+	resp, err := client.Do(req)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/tool/http/http_test.go b/pkg/tool/http/http_test.go
index 6de2640..626746a 100644
--- a/pkg/tool/http/http_test.go
+++ b/pkg/tool/http/http_test.go
@@ -15,13 +15,78 @@
 package http
 
 import (
+	"encoding/pem"
 	"fmt"
+	"net/http"
+	"net/http/httptest"
 	"strings"
 	"testing"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/parser"
+	"cuelang.org/go/internal/task"
+	"cuelang.org/go/internal/value"
 )
 
+func newTLSServer() *httptest.Server {
+	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		resp := `{"foo": "bar"}`
+		w.Write([]byte(resp))
+	}))
+	return server
+}
+
+func parse(t *testing.T, kind, expr string) cue.Value {
+	t.Helper()
+
+	x, err := parser.ParseExpr("test", expr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var r cue.Runtime
+	i, err := r.CompileExpr(x)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return value.UnifyBuiltin(i.Value(), kind)
+}
+
+func TestTLS(t *testing.T) {
+	s := newTLSServer()
+	defer s.Close()
+
+	v1 := parse(t, "tool/http.Get", fmt.Sprintf(`{url: "%s"}`, s.URL))
+	_, err := (*httpCmd).Run(nil, &task.Context{Obj: v1})
+	if err == nil {
+		t.Fatal("http call should have failed")
+	}
+
+	v2 := parse(t, "tool/http.Get", fmt.Sprintf(`{url: "%s", tls: verify: false}`, s.URL))
+	_, err = (*httpCmd).Run(nil, &task.Context{Obj: v2})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	publicKeyBlock := pem.Block{
+		Type:  "PUBLIC KEY",
+		Bytes: s.Certificate().Raw,
+	}
+	publicKeyPem := pem.EncodeToMemory(&publicKeyBlock)
+
+	v3 := parse(t, "tool/http.Get", fmt.Sprintf(`
+	{
+		url: "%s"
+		tls: caCert: '''
+%s
+'''
+	}`, s.URL, publicKeyPem))
+
+	_, err = (*httpCmd).Run(nil, &task.Context{Obj: v3})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestParseHeaders(t *testing.T) {
 	req := `
 	header: {
diff --git a/pkg/tool/http/pkg.go b/pkg/tool/http/pkg.go
index e1683eb..3b8b4e7 100644
--- a/pkg/tool/http/pkg.go
+++ b/pkg/tool/http/pkg.go
@@ -35,6 +35,10 @@
 		$id:    *"tool/http.Do" | "http"
 		method: string
 		url:    string
+		tls: {
+			verify:  *true | bool
+			caCert?: bytes | string
+		}
 		request: {
 			body?: bytes | string
 			header: {