Skip to content

Commit

Permalink
Change client initialization to accept an http.Client
Browse files Browse the repository at this point in the history
This implementation follows the most common best practices around using a RoundTripper for authentication, rather than passing a credential and modifying the Do to apply the credential headers.

Definining a RoundTripper is a more flexible, powerful, and elengant approach.

This change requires to bump to 1.7 (given we add the dependency to golang/x/oauth2 that uses the context package).

Closes GH-15
  • Loading branch information
weppos committed Apr 3, 2018
1 parent ad4a436 commit 943ac1e
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 88 deletions.
5 changes: 0 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
language: go

go:
- "1.2"
- "1.3"
- "1.4"
- "1.5"
- "1.6"
- "1.7"
- "1.8"
- "1.9"
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,22 @@ This library is a Go client you can use to interact with the [DNSimple API v2](h
package main

import (
"context"
"fmt"
"os"
"strconv"

"github.com/dnsimple/dnsimple-go/dnsimple"
"golang.org/x/oauth2"
)

func main() {
oauthToken := "xxxxxxx"
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: oauthToken})
tc := oauth2.NewClient(context.Background(), ts)

// new client
client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(oauthToken))
client := dnsimple.NewClient(tc)

// get the current authenticated account (if you don't know who you are)
whoamiResponse, err := client.Identity.Whoami()
Expand Down Expand Up @@ -72,6 +76,36 @@ func main() {
For more complete documentation, see [godoc](https://godoc.org/github.com/dnsimple/dnsimple-go/dnsimple).


## Authentication

When creating a new client you are required to provide an `http.Client` to use for authenticating the requests.
Currently supported authentication mechanisms are OAuth and HTTP Digest.

For OAuth we suggest to use the client provided by the `golang.org/x/oauth2` package:

```go
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "XXX"})
tc := oauth2.NewClient(context.Background(), ts)

// new client
client := dnsimple.NewClient(tc)
```

For HTTP Digest you can use the client provided by this package:

```
tp := dnsimple.BasicAuthTransport{
Username: "XXX",
Password: "XXX",
}
client := dnsimple.NewClient(tp.Client())
```

For any other custom need you can define your own `http.RoundTripper` implementation and
pass a client that authenticated with the custom round tripper.


## Sandbox Environment

We highly recommend testing against our [sandbox environment](https://developer.dnsimple.com/sandbox/) before using our production environment. This will allow you to avoid real purchases, live charges on your credit card, and reduce the chance of your running up against rate limits.
Expand Down
72 changes: 36 additions & 36 deletions dnsimple/authentication.go
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
package dnsimple

import (
"encoding/base64"
"net/http"
)

const (
httpHeaderAuthorization = "Authorization"
)

// Provides credentials that can be used for authenticating with DNSimple.
//
// See https://developer.dnsimple.com/v2/#authentication
type Credentials interface {
// Returns the HTTP headers that should be set
// to authenticate the HTTP Request.
Headers() map[string]string
}
// BasicAuthTransport is an http.RoundTripper that authenticates all requests
// using HTTP Basic Authentication with the provided username and password.
type BasicAuthTransport struct {
Username string
Password string

// HTTP basic authentication
type httpBasicCredentials struct {
email string
password string
// Transport is the transport RoundTripper used to make HTTP requests.
// If nil, http.DefaultTransport is used.
Transport http.RoundTripper
}

// NewHTTPBasicCredentials construct Credentials using HTTP Basic Auth.
func NewHTTPBasicCredentials(email, password string) Credentials {
return &httpBasicCredentials{email, password}
}

func (c *httpBasicCredentials) Headers() map[string]string {
return map[string]string{httpHeaderAuthorization: "Basic " + c.basicAuth(c.email, c.password)}
}
// RoundTrip implements the RoundTripper interface. We just add the
// basic auth and return the RoundTripper for this transport type.
func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := cloneRequest(req) // per RoundTripper contract

func (c *httpBasicCredentials) basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
req2.SetBasicAuth(t.Username, t.Password)
return t.transport().RoundTrip(req2)
}

// OAuth token authentication
type oauthTokenCredentials struct {
oauthToken string
// Client returns an *http.Client that uses the BasicAuthTransport transport
// to authenticate the request via HTTP Basic Auth.
func (t *BasicAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

// NewOauthTokenCredentials construct Credentials using the OAuth access token.
func NewOauthTokenCredentials(oauthToken string) Credentials {
return &oauthTokenCredentials{oauthToken: oauthToken}
func (t *BasicAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}

func (c *oauthTokenCredentials) Headers() map[string]string {
return map[string]string{httpHeaderAuthorization: "Bearer " + c.oauthToken}
// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
func cloneRequest(r *http.Request) *http.Request {
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}
return r2
}
26 changes: 0 additions & 26 deletions dnsimple/authentication_test.go
Original file line number Diff line number Diff line change
@@ -1,27 +1 @@
package dnsimple

import (
"fmt"
"reflect"
"testing"
)

func testCredentials(t *testing.T, credentials Credentials, headers map[string]string) {
if want, got := headers, credentials.Headers(); !reflect.DeepEqual(want, got) {
t.Errorf("Header %v, want %v", got, want)
}
}

func TestHttpBasicCredentialsHttpHeader(t *testing.T) {
email, password := "email", "password"
credentials := NewHTTPBasicCredentials(email, password)
expectedHeaderValue := "Basic ZW1haWw6cGFzc3dvcmQ="
testCredentials(t, credentials, map[string]string{httpHeaderAuthorization: expectedHeaderValue})
}

func TestOauthTokenCredentialsHttpHeader(t *testing.T) {
oauthToken := "oauth-token"
credentials := NewOauthTokenCredentials(oauthToken)
expectedHeaderValue := fmt.Sprintf("Bearer %v", oauthToken)
testCredentials(t, credentials, map[string]string{httpHeaderAuthorization: expectedHeaderValue})
}
21 changes: 9 additions & 12 deletions dnsimple/dnsimple.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ const (

// Client represents a client to the DNSimple API.
type Client struct {
// HttpClient is the underlying HTTP client
// httpClient is the underlying HTTP client
// used to communicate with the API.
HttpClient *http.Client

// Credentials used for accessing the DNSimple API
Credentials Credentials
httpClient *http.Client

// BaseURL for API requests.
// Defaults to the public DNSimple API, but can be set to a different endpoint (e.g. the sandbox).
Expand Down Expand Up @@ -85,9 +82,12 @@ type ListOptions struct {
Sort string `url:"sort,omitempty"`
}

// NewClient returns a new DNSimple API client using the given credentials.
func NewClient(credentials Credentials) *Client {
c := &Client{Credentials: credentials, HttpClient: &http.Client{}, BaseURL: defaultBaseURL}
// NewClient returns a new DNSimple API client.
//
// To authenticate you must provide an http.Client that will perform authentication
// for you with one of the currently supported mechanisms: OAuth or HTTP Basic.
func NewClient(httpClient *http.Client) *Client {
c := &Client{httpClient: httpClient, BaseURL: defaultBaseURL}
c.Identity = &IdentityService{client: c}
c.Accounts = &AccountsService{client: c}
c.Certificates = &CertificatesService{client: c}
Expand Down Expand Up @@ -126,9 +126,6 @@ func (c *Client) NewRequest(method, path string, payload interface{}) (*http.Req
req.Header.Set("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("User-Agent", formatUserAgent(c.UserAgent))
for key, value := range c.Credentials.Headers() {
req.Header.Add(key, value)
}

return req, nil
}
Expand Down Expand Up @@ -212,7 +209,7 @@ func (c *Client) Do(req *http.Request, obj interface{}) (*http.Response, error)
log.Printf("Executing request (%v): %#v", req.URL, req)
}

resp, err := c.HttpClient.Do(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
Expand Down
10 changes: 5 additions & 5 deletions dnsimple/dnsimple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func setupMockServer() {
mux = http.NewServeMux()
server = httptest.NewServer(mux)

client = NewClient(NewOauthTokenCredentials("dnsimple-token"))
client = NewClient(http.DefaultClient)
client.BaseURL = server.URL
}

Expand Down Expand Up @@ -110,15 +110,15 @@ func httpResponseFixture(t *testing.T, filename string) *http.Response {
}

func TestNewClient(t *testing.T) {
c := NewClient(NewOauthTokenCredentials("dnsimple-token"))
c := NewClient(http.DefaultClient)

if c.BaseURL != defaultBaseURL {
t.Errorf("NewClient BaseURL = %v, want %v", c.BaseURL, defaultBaseURL)
}
}

func TestClient_NewRequest(t *testing.T) {
c := NewClient(NewOauthTokenCredentials("dnsimple-token"))
c := NewClient(http.DefaultClient)
c.BaseURL = "https://go.example.com"

inURL, outURL := "/foo", "https://go.example.com/foo"
Expand All @@ -137,7 +137,7 @@ func TestClient_NewRequest(t *testing.T) {
}

func TestClient_NewRequest_CustomUserAgent(t *testing.T) {
c := NewClient(NewOauthTokenCredentials("dnsimple-token"))
c := NewClient(http.DefaultClient)
c.UserAgent = "AwesomeClient"
req, _ := c.NewRequest("GET", "/", nil)

Expand All @@ -156,7 +156,7 @@ func (o *badObject) MarshalJSON() ([]byte, error) {
}

func TestClient_NewRequest_WithBody(t *testing.T) {
c := NewClient(NewOauthTokenCredentials("dnsimple-token"))
c := NewClient(http.DefaultClient)
c.BaseURL = "https://go.example.com/"

inURL, _ := "foo", "https://go.example.com/v2/foo"
Expand Down
10 changes: 8 additions & 2 deletions dnsimple/live_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package dnsimple

import (
"context"
"fmt"
"os"
"strconv"
"testing"
"time"

"golang.org/x/oauth2"
)

var (
Expand All @@ -19,14 +22,17 @@ func init() {
dnsimpleToken = os.Getenv("DNSIMPLE_TOKEN")
dnsimpleBaseURL = os.Getenv("DNSIMPLE_BASE_URL")

// Prevent peoeple from wiping out their entire production account by mistake
// Prevent people from wiping out their entire production account by mistake
if dnsimpleBaseURL == "" {
dnsimpleBaseURL = "https://api.sandbox.dnsimple.com"
}

if len(dnsimpleToken) > 0 {
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: dnsimpleToken})
tc := oauth2.NewClient(context.Background(), ts)

dnsimpleLiveTest = true
dnsimpleClient = NewClient(NewOauthTokenCredentials(dnsimpleToken))
dnsimpleClient = NewClient(tc)
dnsimpleClient.BaseURL = dnsimpleBaseURL
dnsimpleClient.UserAgent = fmt.Sprintf("%v +livetest", dnsimpleClient.UserAgent)
}
Expand Down
2 changes: 1 addition & 1 deletion dnsimple/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (s *OauthService) ExchangeAuthorizationForToken(authorization *ExchangeAuth
return nil, err
}

resp, err := s.client.HttpClient.Do(req)
resp, err := s.client.httpClient.Do(req)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit 943ac1e

Please sign in to comment.