diff --git a/lib/http.go b/lib/http.go index 6865089..67ced9f 100644 --- a/lib/http.go +++ b/lib/http.go @@ -20,6 +20,7 @@ package lib import ( "bytes" "context" + "encoding/base64" "fmt" "io" "net/http" @@ -42,7 +43,12 @@ import ( // 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. +// 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 // @@ -68,9 +74,9 @@ import ( // // GET Request // -// get returns a GET method request: +// get_request returns a GET method request: // -// get(<string>) -> <map<string,dyn>> +// get_request(<string>) -> <map<string,dyn>> // // Example: // @@ -112,7 +118,7 @@ import ( // // Example: // -// post("http://www.example.com/", "text/plain", "test") +// post_request("http://www.example.com/", "text/plain", "test") // // will return: // @@ -144,7 +150,7 @@ import ( // Example: // // request("GET", "http://www.example.com/").with({"Header":{ -// "Authorization": "Basic "+string(base64("username:password")), +// "Authorization": ["Basic "+string(base64("username:password"))], // }}) // // will return: @@ -153,7 +159,39 @@ import ( // "Close": false, // "ContentLength": 0, // "Header": { -// "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" +// "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", @@ -250,13 +288,13 @@ import ( // // line=25&page=2" // -func HTTP(client *http.Client, limit *rate.Limiter) cel.EnvOption { - return HTTPWithContext(context.Background(), client, limit) +func HTTP(client *http.Client, limit *rate.Limiter, auth *BasicAuth) cel.EnvOption { + return HTTPWithContext(context.Background(), client, limit, auth) } -// HTTP 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) cel.EnvOption { +// 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 { if client == nil { client = http.DefaultClient } @@ -266,6 +304,7 @@ func HTTPWithContext(ctx context.Context, client *http.Client, limit *rate.Limit return cel.Lib(httpLib{ client: client, limit: limit, + auth: auth, ctx: ctx, }) } @@ -273,9 +312,17 @@ func HTTPWithContext(ctx context.Context, client *http.Client, limit *rate.Limit type httpLib struct { client *http.Client limit *rate.Limiter + auth *BasicAuth ctx context.Context } +// 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 (httpLib) CompileOptions() []cel.EnvOption { return []cel.EnvOption{ cel.Declarations( @@ -341,6 +388,13 @@ func (httpLib) CompileOptions() []cel.EnvOption { decls.NewMapType(decls.String, decls.Dyn), ), ), + decls.NewFunction("basic_authentication", + decls.NewInstanceOverload( + "map_basic_authentication_string_string", + []*expr.Type{decls.NewMapType(decls.String, decls.Dyn), decls.String, decls.String}, + decls.NewMapType(decls.String, decls.Dyn), + ), + ), decls.NewFunction("do_request", decls.NewInstanceOverload( "map_do_request", @@ -434,6 +488,12 @@ func (l httpLib) ProgramOptions() []cel.ProgramOption { Function: newRequestBody, }, ), + cel.Functions( + &functions.Overload{ + Operator: "map_basic_authentication_string_string", + Function: l.basicAuthentication, + }, + ), cel.Functions( &functions.Overload{ Operator: "map_do_request", @@ -492,6 +552,9 @@ func (l httpLib) head(url types.String) (*http.Response, error) { if err != nil { return nil, err } + if l.auth != nil { + req.SetBasicAuth(l.auth.Username, l.auth.Password) + } return l.client.Do(req) } @@ -520,6 +583,9 @@ func (l httpLib) get(url types.String) (*http.Response, error) { if err != nil { return nil, err } + if l.auth != nil { + req.SetBasicAuth(l.auth.Username, l.auth.Password) + } return l.client.Do(req) } @@ -572,6 +638,9 @@ func (l httpLib) post(url, content types.String, body io.Reader) (*http.Response if err != nil { return nil, err } + if l.auth != nil { + req.SetBasicAuth(l.auth.Username, l.auth.Password) + } req.Header.Set("Content-Type", string(content)) return l.client.Do(req) } @@ -722,6 +791,49 @@ func respToMap(resp *http.Response) (map[string]interface{}, error) { 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 { diff --git a/mito.go b/mito.go index b2187bf..d89e44a 100644 --- a/mito.go +++ b/mito.go @@ -148,7 +148,7 @@ var ( "try": lib.Try(), "file": lib.File(mimetypes), "mime": lib.MIME(mimetypes), - "http": lib.HTTP(nil, nil), + "http": lib.HTTP(nil, nil, nil), "limit": lib.Limit(limitPolicies), } diff --git a/testdata/basic_auth.txt b/testdata/basic_auth.txt new file mode 100644 index 0000000..cfee57b --- /dev/null +++ b/testdata/basic_auth.txt @@ -0,0 +1,22 @@ +mito -use http src.cel +! stderr . +cmp stdout want.txt + +-- src.cel -- +request("GET", "http://www.example.com/").basic_authentication("username", "password") +-- want.txt -- +{ + "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/" +} diff --git a/testdata/request.txt b/testdata/request.txt index c18b926..fe1cc62 100644 --- a/testdata/request.txt +++ b/testdata/request.txt @@ -7,7 +7,7 @@ cmp stdout want.txt request("GET", "http://www.example.com/"), request("GET", "http://www.example.com/", "request data"), request("GET", "http://www.example.com/").with({"Header":{ - "Authorization": "Basic "+string(base64("username:password")), + "Authorization": ["Basic "+string(base64("username:password"))], }}), get_request("http://www.example.com/"), post_request("http://www.example.com/", "text/plain", "request data"), @@ -41,7 +41,9 @@ cmp stdout want.txt "Close": false, "ContentLength": 0, "Header": { - "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + "Authorization": [ + "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ] }, "Host": "www.example.com", "Method": "GET",