lib/http.go (797 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package lib import ( "bytes" "context" "encoding/base64" "errors" "fmt" "io" "net/http" "net/url" "reflect" "strings" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" "golang.org/x/time/rate" ) // HTTP returns a cel.EnvOption to configure extended functions for HTTP // requests. Requests and responses are returned as maps corresponding to // the Go http.Request and http.Response structs. The client and limit parameters // will be used for the requests and API rate limiting. If client is nil // the http.DefaultClient will be used and if limit is nil an non-limiting // rate.Limiter will be used. If auth is not nil, the Authorization header // is populated for Basic Authentication in requests constructed for direct // HEAD, GET and POST method calls. Explicitly constructed requests used in // do_request are not affected by auth. In cases where Basic Authentication // is needed for these constructed requests, the basic_authentication method // can be used to add the necessary header. // // # HEAD // // head performs a HEAD method request and returns the result: // // head(<string>) -> <map<string,dyn>> // // Example: // // head('http://www.example.com/') // returns {"Body": "", "Close": false, // // # GET // // get performs a GET method request and returns the result: // // get(<string>) -> <map<string,dyn>> // // Example: // // get('http://www.example.com/') // returns {"Body": "PCFkb2N0e... // // # GET Request // // get_request returns a GET method request: // // get_request(<string>) -> <map<string,dyn>> // // Example: // // get_request('http://www.example.com/') // // will return: // // { // "Close": false, // "ContentLength": 0, // "Header": {}, // "Host": "www.example.com", // "Method": "GET", // "Proto": "HTTP/1.1", // "ProtoMajor": 1, // "ProtoMinor": 1, // "URL": "http://www.example.com/" // } // // # POST // // post performs a POST method request and returns the result: // // post(<string>, <string>, <bytes>) -> <map<string,dyn>> // post(<string>, <string>, <string>) -> <map<string,dyn>> // // Example: // // post("http://www.example.com/", "text/plain", "test") // returns {"Body": "PCFkb2N0e... // // # POST Request // // post_request returns a POST method request: // // post_request(<string>, <string>, <bytes>) -> <map<string,dyn>> // post_request(<string>, <string>, <string>) -> <map<string,dyn>> // // Example: // // post_request("http://www.example.com/", "text/plain", "test") // // will return: // // { // "Body": "test", // "Close": false, // "ContentLength": 4, // "Header": { // "Content-Type": [ // "text/plain" // ] // }, // "Host": "www.example.com", // "Method": "POST", // "Proto": "HTTP/1.1", // "ProtoMajor": 1, // "ProtoMinor": 1, // "URL": "http://www.example.com/" // } // // # Request // // request returns a user-defined method request: // // request(<string>, <string>) -> <map<string,dyn>> // request(<string>, <string>, <bytes>) -> <map<string,dyn>> // request(<string>, <string>, <string>) -> <map<string,dyn>> // // Example: // // request("GET", "http://www.example.com/").with({"Header":{ // "Authorization": ["Basic "+string(base64("username:password"))], // }}) // // will return: // // { // "Close": false, // "ContentLength": 0, // "Header": { // "Authorization": [ // "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" // ] // }, // "Host": "www.example.com", // "Method": "GET", // "Proto": "HTTP/1.1", // "ProtoMajor": 1, // "ProtoMinor": 1, // "URL": "http://www.example.com/" // } // // # Basic Authentication // // basic_authentication adds a Basic Authentication Authorization header to a request, // returning the modified request. // // <map<string,dyn>>.basic_authentication(<string>, <string>) -> <map<string,dyn>> // // Example: // // request("GET", "http://www.example.com/").basic_authentication("username", "password") // // will return: // // { // "Close": false, // "ContentLength": 0, // "Header": { // "Authorization": [ // "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" // ] // }, // "Host": "www.example.com", // "Method": "GET", // "Proto": "HTTP/1.1", // "ProtoMajor": 1, // "ProtoMinor": 1, // "URL": "http://www.example.com/" // } // // # Do Request // // do_request executes an HTTP request: // // <map<string,dyn>>.do_request() -> <map<string,dyn>> // // Example: // // get_request("http://www.example.com/").do_request() // returns {"Body": "PCFkb2N0e... // // # Parse URL // // parse_url returns a map holding the details of the parsed URL corresponding // to the Go url.URL struct: // // <string>.parse_url() -> <map<string,dyn>> // // Example: // // "https://pkg.go.dev/net/url#URL".parse_url() // // will return: // // { // "ForceQuery": false, // "Fragment": "URL", // "Host": "pkg.go.dev", // "Opaque": "", // "Path": "/net/url", // "RawFragment": "", // "RawPath": "", // "RawQuery": "", // "Scheme": "https", // "User": null // } // // # Format URL // // format_url returns string corresponding to the URL map that is the receiver: // // <map<string,dyn>>.format_url() -> <string> // // Example: // // "https://pkg.go.dev/net/url#URL".parse_url().with_replace({"Host": "godoc.org"}).format_url() // // will return: // // "https://godoc.org/net/url#URL" // // # Parse Query // // parse_query returns a map holding the details of the parsed query corresponding // to the Go url.Values map: // // <string>.parse_query() -> <map<string,<list<string>>> // // Example: // // "page=1&line=25".parse_url() // // will return: // // { // "line": ["25"], // "page": ["1"] // } // // # Format Query // // format_query returns string corresponding to the query map that is the receiver: // // <map<string,<list<string>>>.format_query() -> <string> // // Example: // // "page=1&line=25".parse_query().with_replace({"page":[string(2)]}).format_query() // // will return: // // line=25&page=2" func HTTP(client *http.Client, limit *rate.Limiter, auth *BasicAuth) cel.EnvOption { return HTTPWithContextOpts(context.Background(), client, HTTPOptions{Limiter: limit, BasicAuth: auth}) } // HTTPWithContext returns a cel.EnvOption to configure extended functions // for HTTP requests that include a context.Context in network requests. func HTTPWithContext(ctx context.Context, client *http.Client, limit *rate.Limiter, auth *BasicAuth) cel.EnvOption { return HTTPWithContextOpts(ctx, client, HTTPOptions{Limiter: limit, BasicAuth: auth}) } // HTTPWithContextOps returns a cel.EnvOption to configure extended functions // for HTTP requests that include a context.Context in network requests and // includes extended client options. func HTTPWithContextOpts(ctx context.Context, client *http.Client, options HTTPOptions) cel.EnvOption { if client == nil { client = http.DefaultClient } if options.Limiter == nil { options.Limiter = rate.NewLimiter(rate.Inf, 0) } return cel.Lib(httpLib{ client: client, options: options, ctx: ctx, }) } // HTTPOptions holds HTTP lib configuration options. type HTTPOptions struct { // Limiter is the rate limiter used by HTTP clients. Limiter *rate.Limiter // BasicAuth is the Basic Authentication configuration // for direct HEAD, GET and POST method calls. BasicAuth *BasicAuth // Headers is the set of headers to be added to an HTTP // request. Headers are added to all method calls for // both direct and constructed requests. Headers values // in Headers are not set if they would overwrite existing // headers in the request. Headers http.Header // MaxBodySize is the largest response body that will be // accepted by the client. If MaxBodySize is zero there is // no limit. Bodies greater than the limit will result in an // ErrBodyTooBig error being returned by the request. MaxBodySize int64 } func (o HTTPOptions) IsZero() bool { return o.Limiter == nil && o.BasicAuth == nil && o.Headers == nil && o.MaxBodySize == 0 } type httpLib struct { client *http.Client ctx context.Context options HTTPOptions } // BasicAuth is used to populate the Authorization header to use HTTP // Basic Authentication with the provided username and password for // direct HTTP method calls. type BasicAuth struct { Username, Password string } func (l httpLib) CompileOptions() []cel.EnvOption { return []cel.EnvOption{ cel.Function("head", cel.Overload( "head_string", []*cel.Type{cel.StringType}, mapStringDyn, cel.UnaryBinding(catch(l.doHead)), ), ), cel.Function("get", cel.Overload( "get_string", []*cel.Type{cel.StringType}, mapStringDyn, cel.UnaryBinding(catch(l.doGet)), ), ), cel.Function("get_request", cel.Overload( "get_request_string", []*cel.Type{cel.StringType}, mapStringDyn, cel.UnaryBinding(catch(l.newGetRequest)), ), ), cel.Function("post", cel.Overload( "post_string_string_bytes", []*cel.Type{cel.StringType, cel.StringType, cel.BytesType}, mapStringDyn, cel.FunctionBinding(catch(l.doPost)), ), cel.Overload( "post_string_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, mapStringDyn, cel.FunctionBinding(catch(l.doPost)), ), ), cel.Function("post_request", cel.Overload( "post_request_string_string_bytes", []*cel.Type{cel.StringType, cel.StringType, cel.BytesType}, mapStringDyn, cel.FunctionBinding(catch(l.newPostRequest)), ), cel.Overload( "post_request_string_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, mapStringDyn, cel.FunctionBinding(catch(l.newPostRequest)), ), ), cel.Function("request", cel.Overload( "request_string_string", []*cel.Type{cel.StringType, cel.StringType}, mapStringDyn, cel.BinaryBinding(catch(l.newRequest)), ), cel.Overload( "request_string_string_bytes", []*cel.Type{cel.StringType, cel.StringType, cel.BytesType}, mapStringDyn, cel.FunctionBinding(catch(l.newRequestBody)), ), cel.Overload( "request_string_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, mapStringDyn, cel.FunctionBinding(catch(l.newRequestBody)), ), ), cel.Function("do_request", cel.MemberOverload( "map_do_request", []*cel.Type{mapStringDyn}, mapStringDyn, cel.UnaryBinding(catch(l.doRequest)), ), ), cel.Function("basic_authentication", cel.MemberOverload( "map_basic_authentication_string_string", []*cel.Type{mapStringDyn, cel.StringType, cel.StringType}, mapStringDyn, cel.FunctionBinding(catch(l.basicAuthentication)), ), ), cel.Function("parse_url", cel.MemberOverload( "string_parse_url", []*cel.Type{cel.StringType}, mapStringDyn, cel.UnaryBinding(catch(parseURL)), ), ), cel.Function("format_url", cel.MemberOverload( "map_format_url", []*cel.Type{mapStringDyn}, cel.StringType, cel.UnaryBinding(catch(formatURL)), ), ), cel.Function("parse_query", cel.MemberOverload( "string_parse_query", []*cel.Type{cel.StringType}, mapStringDyn, cel.UnaryBinding(catch(parseQuery)), ), ), cel.Function("format_query", cel.MemberOverload( "map_format_query", []*cel.Type{mapStringDyn}, cel.StringType, cel.UnaryBinding(catch(formatQuery)), ), ), } } func (httpLib) ProgramOptions() []cel.ProgramOption { return nil } func (l httpLib) doHead(arg ref.Val) ref.Val { url, ok := arg.(types.String) if !ok { return types.ValOrErr(url, "no such overload for head") } err := l.options.Limiter.Wait(context.TODO()) if err != nil { return types.NewErr("%s", err) } resp, err := l.head(url) if err != nil { return types.NewErr("%s", err) } rm, err := respToMap(resp, l.options.MaxBodySize) if err != nil { return types.NewErr("%s", err) } return types.DefaultTypeAdapter.NativeToValue(rm) } func (l httpLib) head(url types.String) (*http.Response, error) { req, err := http.NewRequestWithContext(l.ctx, http.MethodHead, string(url), nil) if err != nil { return nil, err } if l.options.BasicAuth != nil { req.SetBasicAuth(l.options.BasicAuth.Username, l.options.BasicAuth.Password) } addHeaders(req, l.options.Headers) return l.client.Do(req) } func (l httpLib) doGet(arg ref.Val) ref.Val { url, ok := arg.(types.String) if !ok { return types.ValOrErr(url, "no such overload for get") } err := l.options.Limiter.Wait(context.TODO()) if err != nil { return types.NewErr("%s", err) } resp, err := l.get(url) if err != nil { return types.NewErr("%s", err) } rm, err := respToMap(resp, l.options.MaxBodySize) if err != nil { return types.NewErr("%s", err) } return types.DefaultTypeAdapter.NativeToValue(rm) } func (l httpLib) get(url types.String) (*http.Response, error) { req, err := http.NewRequestWithContext(l.ctx, http.MethodGet, string(url), nil) if err != nil { return nil, err } if l.options.BasicAuth != nil { req.SetBasicAuth(l.options.BasicAuth.Username, l.options.BasicAuth.Password) } addHeaders(req, l.options.Headers) return l.client.Do(req) } func (l httpLib) newGetRequest(url ref.Val) ref.Val { return l.newRequestBody(types.String("GET"), url) } func (l httpLib) doPost(args ...ref.Val) ref.Val { if len(args) != 3 { return types.NewErr("no such overload for post") } url, ok := args[0].(types.String) if !ok { return types.ValOrErr(url, "no such overload for request") } content, ok := args[1].(types.String) if !ok { return types.ValOrErr(content, "no such overload for request") } var body io.Reader switch text := args[2].(type) { case types.Bytes: if len(text) != 0 { body = bytes.NewReader(text) } case types.String: if text != "" { body = strings.NewReader(string(text)) } default: return types.NewErr("invalid type for post body: %s", text.Type()) } err := l.options.Limiter.Wait(context.TODO()) if err != nil { return types.NewErr("%s", err) } resp, err := l.post(url, content, body) if err != nil { return types.NewErr("%s", err) } rm, err := respToMap(resp, l.options.MaxBodySize) if err != nil { return types.NewErr("%s", err) } return types.DefaultTypeAdapter.NativeToValue(rm) } func (l httpLib) post(url, content types.String, body io.Reader) (*http.Response, error) { req, err := http.NewRequestWithContext(l.ctx, http.MethodPost, string(url), body) if err != nil { return nil, err } if l.options.BasicAuth != nil { req.SetBasicAuth(l.options.BasicAuth.Username, l.options.BasicAuth.Password) } req.Header.Set("Content-Type", string(content)) addHeaders(req, l.options.Headers) return l.client.Do(req) } func (l httpLib) newPostRequest(args ...ref.Val) ref.Val { if len(args) != 3 { return types.NewErr("no such overload for post request") } content, ok := args[1].(types.String) if !ok { return types.ValOrErr(content, "no such overload for request") } url := args[0] body := args[2] req, err := makeRequestBody(l.options.MaxBodySize, types.String("POST"), url, body) if err != nil { return err } h, ok := req["Header"] if !ok { h = make(http.Header) req["Header"] = h } h.(http.Header).Set("Content-Type", string(content)) return types.DefaultTypeAdapter.NativeToValue(req) } func (l httpLib) newRequest(method, url ref.Val) ref.Val { return l.newRequestBody(method, url) } func (l httpLib) newRequestBody(args ...ref.Val) ref.Val { req, err := makeRequestBody(l.options.MaxBodySize, args...) if err != nil { return err } return types.DefaultTypeAdapter.NativeToValue(req) } func makeRequestBody(max int64, args ...ref.Val) (map[string]interface{}, ref.Val) { if len(args) < 2 { return nil, types.NewErr("no such overload for request") } method, ok := args[0].(types.String) if !ok { return nil, types.ValOrErr(method, "no such overload for request") } url, ok := args[1].(types.String) if !ok { return nil, types.ValOrErr(method, "no such overload for request") } var ( body ref.Val bodyReader io.Reader ) if len(args) == 3 { body = args[2] switch body := body.(type) { case types.Bytes: if len(body) != 0 { bodyReader = bytes.NewReader(body) } case types.String: if body != "" { bodyReader = strings.NewReader(string(body)) } default: return nil, types.NewErr("invalid type for request body: %s", body.Type()) } } req, err := http.NewRequest(string(method), string(url), bodyReader) if err != nil { return nil, types.NewErr("%s", err) } reqMap, err := reqToMap(req, url, body, max) if err != nil { return nil, types.NewErr("%s", err) } return reqMap, nil } func reqToMap(req *http.Request, url, body ref.Val, max int64) (map[string]interface{}, error) { rm := map[string]interface{}{ "Method": req.Method, "URL": url, "Proto": req.Proto, "ProtoMajor": req.ProtoMajor, "ProtoMinor": req.ProtoMinor, "Header": req.Header, "ContentLength": req.ContentLength, "Close": req.Close, "Host": req.Host, } if req.RequestURI != "" { rm["RequestURI"] = req.RequestURI } if body != nil { rm["Body"] = body } if req.TransferEncoding != nil { rm["TransferEncoding"] = req.TransferEncoding } if req.Trailer != nil { rm["Trailer"] = req.Trailer } if req.Response != nil { resp, err := respToMap(req.Response, max) if err != nil { return nil, err } rm["Response"] = resp } return rm, nil } func respToMap(resp *http.Response, max int64) (map[string]interface{}, error) { rm := map[string]interface{}{ "Status": resp.Status, "StatusCode": resp.StatusCode, "Proto": resp.Proto, "ProtoMajor": resp.ProtoMajor, "ProtoMinor": resp.ProtoMinor, "Header": resp.Header, "ContentLength": resp.ContentLength, "Close": resp.Close, "Uncompressed": resp.Uncompressed, } var buf bytes.Buffer _, err := io.Copy(&buf, limitBody(resp.Body, max)) resp.Body.Close() if err != nil { return nil, err } rm["Body"] = buf.Bytes() if resp.TransferEncoding != nil { rm["TransferEncoding"] = resp.TransferEncoding } if resp.Trailer != nil { rm["Trailer"] = resp.Trailer } if resp.Request != nil { req, err := reqToMap(resp.Request, types.String(resp.Request.URL.String()), nil, max) if err != nil { return nil, err } rm["Request"] = req } return rm, nil } func (l httpLib) basicAuthentication(args ...ref.Val) ref.Val { if len(args) != 3 { return types.NewErr("no such overload for request") } request, ok := args[0].(traits.Mapper) if !ok { return types.ValOrErr(request, "no such overload for do_request") } username, ok := args[1].(types.String) if !ok { return types.ValOrErr(username, "no such overload for request") } password, ok := args[2].(types.String) if !ok { return types.ValOrErr(password, "no such overload for request") } reqm, err := request.ConvertToNative(reflectMapStringAnyType) if err != nil { return types.NewErr("%s", err) } // Rather than round-tripping though an http.Request, just // add the Authorization header into the map directly. // This reduces work required in the general case, and greatly // simplifies the case where a body has already been added // to the request. req := reqm.(map[string]interface{}) var header http.Header switch h := req["Header"].(type) { case nil: header = make(http.Header) req["Header"] = header case map[string][]string: header = h case http.Header: header = h default: return types.NewErr("invalid type in header field: %T", h) } header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(username+":"+password))) return types.DefaultTypeAdapter.NativeToValue(req) } func (l httpLib) doRequest(arg ref.Val) ref.Val { request, ok := arg.(traits.Mapper) if !ok { return types.ValOrErr(request, "no such overload for do_request") } reqm, err := request.ConvertToNative(reflectMapStringAnyType) if err != nil { return types.NewErr("%s", err) } req, err := mapToReq(reqm.(map[string]interface{})) if err != nil { return types.NewErr("%s", err) } // Recover the context lost during serialisation to JSON. req = req.WithContext(l.ctx) err = l.options.Limiter.Wait(l.ctx) if err != nil { return types.NewErr("%s", err) } addHeaders(req, l.options.Headers) resp, err := l.client.Do(req) if err != nil { return types.NewErr("%s", err) } respm, err := respToMap(resp, l.options.MaxBodySize) if err != nil { return types.NewErr("%s", err) } return types.DefaultTypeAdapter.NativeToValue(respm) } func mapToReq(rm map[string]interface{}) (*http.Request, error) { if rm == nil { return nil, nil } req := &http.Request{} err := mapConv(reflect.ValueOf(req).Elem(), rm) return req, err } func mapToResp(rm map[string]interface{}) (*http.Response, error) { if rm == nil { return nil, nil } resp := &http.Response{} err := mapConv(reflect.ValueOf(resp).Elem(), rm) return resp, err } func mapConv(dst reflect.Value, src map[string]interface{}) error { rt := dst.Type() for i := 0; i < dst.NumField(); i++ { ft := rt.Field(i) if !ft.IsExported() { continue } v, ok := src[ft.Name] if !ok { continue } conv, ok := convFuncs[ft.Type.String()] if !ok { continue } val, err := conv(reflect.ValueOf(v)) if err != nil { return err } dst.Field(i).Set(val) } return nil } var convFuncs = map[string]func(val reflect.Value) (reflect.Value, error){ "int": func(val reflect.Value) (reflect.Value, error) { return val.Convert(reflectIntType), nil }, "int64": func(val reflect.Value) (reflect.Value, error) { return val.Convert(reflectInt64Type), nil }, "bool": func(val reflect.Value) (reflect.Value, error) { return val.Convert(reflectBoolType), nil }, "string": func(val reflect.Value) (reflect.Value, error) { return val.Convert(reflectStringType), nil }, "[]string": makeStrings, "io.ReadCloser": makeBody, "*url.URL": makeURL, "http.Header": makeMapStrings, "url.Values": makeMapStrings, "*multipart.Form": func(val reflect.Value) (reflect.Value, error) { panic("TODO") }, "*tls.ConnectionState": func(val reflect.Value) (reflect.Value, error) { panic("TODO") }, // These should pass through without this being implemented, but mark them. "*http.Request": func(val reflect.Value) (reflect.Value, error) { panic("REPORT BUG: http.Request") }, "*http.Response": func(val reflect.Value) (reflect.Value, error) { panic("REPORT BUG: http.Response") }, } func makeMapStrings(val reflect.Value) (reflect.Value, error) { iface := val.Interface() switch iface := iface.(type) { case http.Header: return reflect.ValueOf(iface), nil case url.Values: return reflect.ValueOf(iface), nil case map[string][]string: return reflect.ValueOf(iface), nil case map[ref.Val]ref.Val: val := types.DefaultTypeAdapter.NativeToValue(iface) v, err := val.ConvertToNative(reflectMapStringStringSliceType) if err != nil { return reflect.Value{}, err } return reflect.ValueOf(v), nil case ref.Val: v, err := iface.ConvertToNative(reflectMapStringStringSliceType) if err != nil { return reflect.Value{}, err } return reflect.ValueOf(v.(map[string][]string)), nil default: return reflect.Value{}, fmt.Errorf("invalid type: %T", iface) } } func makeStrings(val reflect.Value) (reflect.Value, error) { iface := val.Interface() switch iface := iface.(type) { case []string: return reflect.ValueOf(iface), nil case []types.String: dst := make([]string, len(iface)) for i, s := range iface { dst[i] = string(s) } return reflect.ValueOf(dst), nil case ref.Val: v, err := iface.ConvertToNative(reflectStringSliceType) if err != nil { return reflect.Value{}, err } return reflect.ValueOf(v), nil case []ref.Val: dst := make([]string, len(iface)) for i, s := range iface { v, err := s.ConvertToNative(reflectStringType) if err != nil { return reflect.Value{}, err } dst[i] = v.(string) } return reflect.ValueOf(dst), nil default: return reflect.Value{}, fmt.Errorf("invalid type: %T", iface) } } func makeBody(val reflect.Value) (reflect.Value, error) { var r io.Reader switch val.Kind() { case reflect.String: r = strings.NewReader(val.String()) case reflect.Slice: if !val.CanConvert(reflectByteSliceType) { return reflect.Value{}, fmt.Errorf("invalid type: %s", val.Type()) } r = bytes.NewReader(val.Bytes()) default: return reflect.Value{}, fmt.Errorf("invalid type: %s", val.Type()) } return reflect.ValueOf(io.NopCloser(r)), nil } func makeURL(val reflect.Value) (reflect.Value, error) { if val.Kind() != reflect.String { return reflect.Value{}, fmt.Errorf("invalid type: %s", val.Type()) } u, err := url.Parse(val.String()) if err != nil { return reflect.Value{}, err } return reflect.ValueOf(u), nil } func parseURL(arg ref.Val) ref.Val { addr, ok := arg.(types.String) if !ok { return types.ValOrErr(addr, "no such overload for request") } u, err := url.Parse(string(addr)) if err != nil { return types.NewErr("%s", err) } var user interface{} if u.User != nil { password, passwordSet := u.User.Password() user = map[string]interface{}{ "Username": u.User.Username(), "Password": password, "PasswordSet": passwordSet, } } return types.NewStringInterfaceMap(types.DefaultTypeAdapter, map[string]interface{}{ "Scheme": u.Scheme, "Opaque": u.Opaque, "User": user, "Host": u.Host, "Path": u.Path, "RawPath": u.RawPath, "ForceQuery": u.ForceQuery, "RawQuery": u.RawQuery, "Fragment": u.Fragment, "RawFragment": u.RawFragment, }) } func formatURL(arg ref.Val) ref.Val { urlMap, ok := arg.(traits.Mapper) if !ok { return types.ValOrErr(urlMap, "no such overload") } v, err := urlMap.ConvertToNative(reflectMapStringAnyType) if err != nil { return types.NewErr("no such overload for format_url: %v", err) } m, ok := v.(map[string]interface{}) if !ok { // This should never happen. return types.NewErr("unexpected type for url map: %T", v) } u := url.URL{ Scheme: maybeStringLookup(m, "Scheme"), Opaque: maybeStringLookup(m, "Opaque"), Host: maybeStringLookup(m, "Host"), Path: maybeStringLookup(m, "Path"), RawPath: maybeStringLookup(m, "RawPath"), ForceQuery: maybeBoolLookup(m, "ForceQuery"), RawQuery: maybeStringLookup(m, "RawQuery"), Fragment: maybeStringLookup(m, "Fragment"), RawFragment: maybeStringLookup(m, "RawFragment"), } user, ok := urlMap.Find(types.String("User")) if ok { switch user := user.(type) { case nil: case traits.Mapper: var username types.String un, ok := user.Find(types.String("Username")) if ok { username, ok = un.(types.String) if !ok { return types.NewErr("invalid type for username: %s", un.Type()) } } if user.Get(types.String("PasswordSet")) == types.True { var password types.String pw, ok := user.Find(types.String("Password")) if ok { password, ok = pw.(types.String) if !ok { return types.NewErr("invalid type for password: %s", pw.Type()) } } u.User = url.UserPassword(string(username), string(password)) } else { u.User = url.User(string(username)) } default: if user != types.NullValue { return types.NewErr("unsupported type: %T", user) } } } return types.String(u.String()) } // maybeStringLookup returns a string from m[key] if it is present and the // empty string if not. It panics is m[key] is not a string. func maybeStringLookup(m map[string]interface{}, key string) string { v, ok := m[key] if !ok { return "" } return v.(string) } // maybeBoolLookup returns a bool from m[key] if it is present and false if // not. It panics is m[key] is not a bool. func maybeBoolLookup(m map[string]interface{}, key string) bool { v, ok := m[key] if !ok { return false } return v.(bool) } func parseQuery(arg ref.Val) ref.Val { query, ok := arg.(types.String) if !ok { return types.ValOrErr(query, "no such overload") } q, err := url.ParseQuery(string(query)) if err != nil { return types.NewErr("%s", err) } return types.DefaultTypeAdapter.NativeToValue(q) } func formatQuery(arg ref.Val) ref.Val { queryMap, ok := arg.(traits.Mapper) if !ok { return types.ValOrErr(queryMap, "no such overload") } q, err := queryMap.ConvertToNative(reflectMapStringStringSliceType) if err != nil { return types.NewErr("no such overload for format_query: %v", err) } switch q := q.(type) { case url.Values: return types.String(url.Values(q).Encode()) case map[string][]string: return types.String(url.Values(q).Encode()) default: return types.NewErr("invalid type for format_query: %T", q) } } func addHeaders(dst *http.Request, h http.Header) { for k, v := range h { if _, ok := dst.Header[k]; ok { continue } dst.Header[k] = v } } // ErrBodyTooBig is returned by HTTP requests than are responded to with // a body that is bigger than [HTTPOptions.MaxBodySize] if it is non zero. var ErrBodyTooBig = errors.New("response body too big") // limitBody returns an io.ReadCloser that reads from r, // but stops with ErrBodyTooBig after n bytes unless n is zero. func limitBody(r io.ReadCloser, n int64) io.ReadCloser { if n == 0 { return r } return &limitedReadCloser{r, n} } type limitedReadCloser struct { rc io.ReadCloser n int64 } func (l *limitedReadCloser) Read(p []byte) (n int, err error) { if l.n <= 0 { return 0, ErrBodyTooBig } if int64(len(p)) > l.n { p = p[:l.n] } n, err = l.rc.Read(p) l.n -= int64(n) return } func (l *limitedReadCloser) Close() error { return l.rc.Close() }