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 }