Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: add Basic Authentication to HTTP extension #19

Merged
merged 2 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 123 additions & 11 deletions lib/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package lib
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
Expand All @@ -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
//
Expand All @@ -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:
//
Expand Down Expand Up @@ -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:
//
Expand Down Expand Up @@ -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:
Expand All @@ -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",
Expand Down Expand Up @@ -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
}
Expand All @@ -266,16 +304,25 @@ func HTTPWithContext(ctx context.Context, client *http.Client, limit *rate.Limit
return cel.Lib(httpLib{
client: client,
limit: limit,
auth: auth,
ctx: ctx,
})
}

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(
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion mito.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}

Expand Down
22 changes: 22 additions & 0 deletions testdata/basic_auth.txt
Original file line number Diff line number Diff line change
@@ -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/"
}
6 changes: 4 additions & 2 deletions testdata/request.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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",
Expand Down