cuelang.org/go@v0.13.0/pkg/tool/http/http.go (about)

     1  // Copyright 2019 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package http
    16  
    17  import (
    18  	"bytes"
    19  	"crypto/tls"
    20  	"crypto/x509"
    21  	"encoding/pem"
    22  	"io"
    23  	"net/http"
    24  
    25  	"cuelang.org/go/cue"
    26  	"cuelang.org/go/cue/errors"
    27  	"cuelang.org/go/internal/task"
    28  )
    29  
    30  func init() {
    31  	task.Register("tool/http.Do", newHTTPCmd)
    32  
    33  	// For backwards compatibility.
    34  	task.Register("http", newHTTPCmd)
    35  }
    36  
    37  type httpCmd struct{}
    38  
    39  func newHTTPCmd(v cue.Value) (task.Runner, error) {
    40  	return &httpCmd{}, nil
    41  }
    42  
    43  func (c *httpCmd) Run(ctx *task.Context) (res interface{}, err error) {
    44  	var header, trailer http.Header
    45  	var (
    46  		method = ctx.String("method")
    47  		u      = ctx.String("url")
    48  	)
    49  	var r io.Reader
    50  	if obj := ctx.Obj.LookupPath(cue.MakePath(cue.Str("request"))); obj.Exists() {
    51  		if v := obj.LookupPath(cue.MakePath(cue.Str("body"))); v.Exists() {
    52  			r, err = v.Reader()
    53  			if err != nil {
    54  				return nil, err
    55  			}
    56  		} else {
    57  			r = bytes.NewReader([]byte(""))
    58  		}
    59  		if header, err = parseHeaders(obj, "header"); err != nil {
    60  			return nil, err
    61  		}
    62  		if trailer, err = parseHeaders(obj, "trailer"); err != nil {
    63  			return nil, err
    64  		}
    65  	}
    66  
    67  	var caCert []byte
    68  	caCertValue := ctx.Obj.LookupPath(cue.MakePath(cue.Str("tls"), cue.Str("caCert")))
    69  	if caCertValue.Exists() {
    70  		caCert, err = caCertValue.Bytes()
    71  		if err != nil {
    72  			return nil, errors.Wrapf(err, caCertValue.Pos(), "invalid bytes value")
    73  		}
    74  	}
    75  
    76  	tlsVerify := true
    77  	tlsVerifyValue := ctx.Obj.LookupPath(cue.MakePath(cue.Str("tls"), cue.Str("verify")))
    78  	if tlsVerifyValue.Exists() {
    79  		tlsVerify, err = tlsVerifyValue.Bool()
    80  		if err != nil {
    81  			return nil, errors.Wrapf(err, tlsVerifyValue.Pos(), "invalid bool value")
    82  		}
    83  	}
    84  
    85  	if ctx.Err != nil {
    86  		return nil, ctx.Err
    87  	}
    88  
    89  	transport := http.DefaultTransport.(*http.Transport).Clone()
    90  	transport.TLSClientConfig = &tls.Config{}
    91  
    92  	if !tlsVerify {
    93  		transport.TLSClientConfig.InsecureSkipVerify = true
    94  	}
    95  	if tlsVerify && len(caCert) > 0 {
    96  		pool := x509.NewCertPool()
    97  		for {
    98  			block, rest := pem.Decode(caCert)
    99  			if block == nil {
   100  				break
   101  			}
   102  			if block.Type == "PUBLIC KEY" {
   103  				c, err := x509.ParseCertificate(block.Bytes)
   104  				if err != nil {
   105  					return nil, errors.Wrapf(err, ctx.Obj.Pos(), "failed to parse caCert")
   106  				}
   107  				pool.AddCert(c)
   108  			}
   109  			caCert = rest
   110  		}
   111  		transport.TLSClientConfig.RootCAs = pool
   112  	}
   113  
   114  	client := &http.Client{
   115  		Transport: transport,
   116  		// TODO: timeout
   117  	}
   118  
   119  	// Rather clumsily, we need to also default followRedirects here because
   120  	// it's still valid for tasks to be specified via the special $id field, in
   121  	// which case we cannot be clear that the documented CUE-based defaults have
   122  	// been applied.
   123  	//
   124  	// This is noted as something to fix, more precisely a mistake not to make
   125  	// again, in https://cuelang.org/issue/1325
   126  	followRedirects := true
   127  	followRedirectsValue := ctx.Obj.LookupPath(cue.MakePath(cue.Str("followRedirects")))
   128  	if followRedirectsValue.Exists() {
   129  		var err error
   130  		followRedirects, err = followRedirectsValue.Bool()
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  	}
   135  
   136  	if !followRedirects {
   137  		client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   138  			return http.ErrUseLastResponse
   139  		}
   140  	}
   141  
   142  	req, err := http.NewRequest(method, u, r)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	req.Header = header
   147  	req.Trailer = trailer
   148  
   149  	// TODO: retry logic
   150  	resp, err := client.Do(req)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	defer resp.Body.Close()
   155  	b, err := io.ReadAll(resp.Body)
   156  	// parse response body and headers
   157  	return map[string]interface{}{
   158  		"response": map[string]interface{}{
   159  			"status":     resp.Status,
   160  			"statusCode": resp.StatusCode,
   161  			"body":       string(b),
   162  			"header":     resp.Header,
   163  			"trailer":    resp.Trailer,
   164  		},
   165  	}, err
   166  }
   167  
   168  func parseHeaders(obj cue.Value, label string) (http.Header, error) {
   169  	m := obj.LookupPath(cue.MakePath(cue.Str(label)))
   170  	if !m.Exists() {
   171  		return nil, nil
   172  	}
   173  	iter, err := m.Fields()
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	h := http.Header{}
   178  	for iter.Next() {
   179  		str, err := iter.Value().String()
   180  		if err != nil {
   181  			return nil, err
   182  		}
   183  		h.Add(iter.Selector().Unquoted(), str)
   184  	}
   185  	return h, nil
   186  }