Skip to content

Commit

Permalink
Allow HTTP clients to be configured for auth and reply requests (#64)
Browse files Browse the repository at this point in the history
* Allow HTTP clients to be configured for auth and reply requests

* Move AuthClient and ReplyClient to the ClientConfig for easier plumbing

* Add AuthClient and ReplyClient to AdapterSetting so it can be configured for a new BotFrameworkAdapter

* Require context to be passed so HTTP requests can be made in context

* Ensure token is fetched within context
  • Loading branch information
bmorton authored Jan 11, 2022
1 parent c887952 commit a22d75a
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 29 deletions.
40 changes: 23 additions & 17 deletions connector/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package client

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
Expand All @@ -38,9 +39,9 @@ import (

// Client provides interface to send requests to the connector service.
type Client interface {
Post(url url.URL, activity schema.Activity) error
Delete(url url.URL, activity schema.Activity) error
Put(url url.URL, activity schema.Activity) error
Post(ctx context.Context, url url.URL, activity schema.Activity) error
Delete(ctx context.Context, url url.URL, activity schema.Activity) error
Put(ctx context.Context, url url.URL, activity schema.Activity) error
}

// ConnectorClient implements Client to send HTTP requests to the connector service.
Expand All @@ -56,19 +57,27 @@ func NewClient(config *Config) (Client, error) {
return nil, errors.New("Invalid client configuration")
}

if config.AuthClient == nil {
config.AuthClient = &http.Client{}
}

if config.ReplyClient == nil {
config.ReplyClient = &http.Client{}
}

return &ConnectorClient{*config, cache.AuthCache{}}, nil
}

// Post an activity to given URL.
//
// Creates a HTTP POST request with the provided activity as the body and a Bearer token in the header.
// Returns any error as received from the call to connector service.
func (client *ConnectorClient) Post(target url.URL, activity schema.Activity) error {
func (client *ConnectorClient) Post(ctx context.Context, target url.URL, activity schema.Activity) error {
jsonStr, err := json.Marshal(activity)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, target.String(), bytes.NewBuffer(jsonStr))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewBuffer(jsonStr))
if err != nil {
return err
}
Expand All @@ -79,8 +88,8 @@ func (client *ConnectorClient) Post(target url.URL, activity schema.Activity) er
//
// Creates a HTTP DELETE request with the provided activity ID and a Bearer token in the header.
// Returns any error as received from the call to connector service.
func (client *ConnectorClient) Delete(target url.URL, activity schema.Activity) error {
req, err := http.NewRequest(http.MethodDelete, target.String(), nil)
func (client *ConnectorClient) Delete(ctx context.Context, target url.URL, activity schema.Activity) error {
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, target.String(), nil)
if err != nil {
return err
}
Expand All @@ -91,30 +100,28 @@ func (client *ConnectorClient) Delete(target url.URL, activity schema.Activity)
//
// Creates a HTTP PUT request with the provided activity payload and a Bearer token in the header.
// Returns any error as received from the call to connector service.
func (client *ConnectorClient) Put(target url.URL, activity schema.Activity) error {
func (client *ConnectorClient) Put(ctx context.Context, target url.URL, activity schema.Activity) error {
jsonStr, err := json.Marshal(activity)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPut, target.String(), bytes.NewBuffer(jsonStr))
req, err := http.NewRequestWithContext(ctx, http.MethodPut, target.String(), bytes.NewBuffer(jsonStr))
if err != nil {
return err
}
return client.sendRequest(req, activity)
}

func (client *ConnectorClient) sendRequest(req *http.Request, activity schema.Activity) error {
token, err := client.getToken()
token, err := client.getToken(req.Context())
if err != nil {
return err
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)

replyClient := &http.Client{}

return client.checkRespError(replyClient.Do(req))
return client.checkRespError(client.ReplyClient.Do(req))
}

func (client *ConnectorClient) checkRespError(resp *http.Response, err error) error {
Expand All @@ -138,7 +145,7 @@ func (client *ConnectorClient) checkRespError(resp *http.Response, err error) er
}
}

func (client *ConnectorClient) getToken() (string, error) {
func (client *ConnectorClient) getToken(ctx context.Context) (string, error) {

// Return cached JWT
if !client.AuthCache.IsExpired() {
Expand All @@ -157,16 +164,15 @@ func (client *ConnectorClient) getToken() (string, error) {
return "", err
}

authClient := &http.Client{}
r, err := http.NewRequest("POST", u.String(), strings.NewReader(data.Encode()))
r, err := http.NewRequestWithContext(ctx, "POST", u.String(), strings.NewReader(data.Encode()))
if err != nil {
return "", err
}

r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))

resp, err := authClient.Do(r)
resp, err := client.AuthClient.Do(r)
if err != nil {
return "", customerror.HTTPError{
StatusCode: resp.StatusCode,
Expand Down
3 changes: 3 additions & 0 deletions connector/client/client_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ package client
import (
"errors"
"github.com/infracloudio/msbotbuilder-go/connector/auth"
"net/http"
"net/url"
)

// Config represents the credentials for a user program and the URL for validating the credentials.
type Config struct {
Credentials auth.CredentialProvider
AuthURL url.URL
AuthClient *http.Client
ReplyClient *http.Client
}

// NewClientConfig creates configuration for ConnectorClient.
Expand Down
19 changes: 10 additions & 9 deletions core/activity/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package activity

import (
"context"
"fmt"
"net/url"
"path"
Expand All @@ -31,9 +32,9 @@ import (

// Response provides functionalities to send activity to the connector service.
type Response interface {
SendActivity(activity schema.Activity) error
DeleteActivity(activity schema.Activity) error
UpdateActivity(activity schema.Activity) error
SendActivity(ctx context.Context, activity schema.Activity) error
DeleteActivity(ctx context.Context, activity schema.Activity) error
UpdateActivity(ctx context.Context, activity schema.Activity) error
}

const (
Expand All @@ -50,7 +51,7 @@ type DefaultResponse struct {
}

// DeleteActivity sends a Delete activity method to the BOT connector service.
func (response *DefaultResponse) DeleteActivity(activity schema.Activity) error {
func (response *DefaultResponse) DeleteActivity(ctx context.Context, activity schema.Activity) error {
u, err := url.Parse(activity.ServiceURL)
if err != nil {
return errors.Wrapf(err, "Failed to parse ServiceURL %s.", activity.ServiceURL)
Expand All @@ -60,12 +61,12 @@ func (response *DefaultResponse) DeleteActivity(activity schema.Activity) error

// Send activity to client
u.Path = path.Join(u.Path, respPath)
err = response.Client.Delete(*u, activity)
err = response.Client.Delete(ctx, *u, activity)
return errors.Wrap(err, "Failed to delete response.")
}

// SendActivity sends an activity to the BOT connector service.
func (response *DefaultResponse) SendActivity(activity schema.Activity) error {
func (response *DefaultResponse) SendActivity(ctx context.Context, activity schema.Activity) error {
u, err := url.Parse(activity.ServiceURL)
if err != nil {
return errors.Wrapf(err, "Failed to parse ServiceURL %s.", activity.ServiceURL)
Expand All @@ -80,12 +81,12 @@ func (response *DefaultResponse) SendActivity(activity schema.Activity) error {

// Send activity to client
u.Path = path.Join(u.Path, respPath)
err = response.Client.Post(*u, activity)
err = response.Client.Post(ctx, *u, activity)
return errors.Wrap(err, "Failed to send response.")
}

// UpdateActivity sends a Put activity method to the BOT connector service.
func (response *DefaultResponse) UpdateActivity(activity schema.Activity) error {
func (response *DefaultResponse) UpdateActivity(ctx context.Context, activity schema.Activity) error {
u, err := url.Parse(activity.ServiceURL)
if err != nil {
return errors.Wrapf(err, "Failed to parse ServiceURL %s.", activity.ServiceURL)
Expand All @@ -95,7 +96,7 @@ func (response *DefaultResponse) UpdateActivity(activity schema.Activity) error

// Send activity to client
u.Path = path.Join(u.Path, respPath)
err = response.Client.Put(*u, activity)
err = response.Client.Put(ctx, *u, activity)
return errors.Wrap(err, "Failed to update response.")
}

Expand Down
16 changes: 13 additions & 3 deletions core/bot_framework_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type AdapterSetting struct {
OpenIDMetadata string
ChannelService string
CredentialProvider auth.CredentialProvider
AuthClient *http.Client
ReplyClient *http.Client
}

// BotFrameworkAdapter implements Adapter and is currently the only implementation returned to the user program.
Expand Down Expand Up @@ -77,6 +79,14 @@ func NewBotAdapter(settings AdapterSetting) (Adapter, error) {
return nil, err
}

if settings.AuthClient != nil {
clientConfig.AuthClient = settings.AuthClient
}

if settings.ReplyClient != nil {
clientConfig.ReplyClient = settings.ReplyClient
}

connectorClient, err := client.NewClient(clientConfig)
if err != nil {
return nil, errors.Wrap(err, "Failed to create Connector Client.")
Expand All @@ -102,7 +112,7 @@ func (bf *BotFrameworkAdapter) ProcessActivity(ctx context.Context, req schema.A
return errors.Wrap(err, "Failed to create response object.")
}

return response.SendActivity(replyActivity)
return response.SendActivity(ctx, replyActivity)
}

// ProactiveMessage sends activity to a conversation.
Expand All @@ -124,7 +134,7 @@ func (bf *BotFrameworkAdapter) DeleteActivity(ctx context.Context, activityID st
return errors.Wrap(err, "Failed to create response object.")
}

return response.DeleteActivity(req)
return response.DeleteActivity(ctx, req)
}

// ParseRequest parses the received activity in a HTTP reuqest to:
Expand Down Expand Up @@ -164,5 +174,5 @@ func (bf *BotFrameworkAdapter) UpdateActivity(ctx context.Context, req schema.Ac
if err != nil {
return errors.Wrap(err, "Failed to create response object.")
}
return response.UpdateActivity(req)
return response.UpdateActivity(ctx, req)
}

0 comments on commit a22d75a

Please sign in to comment.