Skip to content

Commit

Permalink
Merge pull request #12 from 3scale/expose-limit-extensions
Browse files Browse the repository at this point in the history
client: Expose rate limit extensions in response
  • Loading branch information
philipgough authored Feb 25, 2019
2 parents ae45048 + 7398eb2 commit e54ff7d
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 21 deletions.
2 changes: 1 addition & 1 deletion client/auth_rep.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (client *ThreeScaleClient) authRep(values url.Values, extensions map[string
}

req.URL.RawQuery = values.Encode()
resp, err = client.doHttpReq(req)
resp, err = client.doHttpReq(req, extensions)
if err != nil {
return resp, fmt.Errorf("error calling 3Scale API - %s", err.Error())
}
Expand Down
1 change: 1 addition & 0 deletions client/auth_rep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func TestAuthRepKey(t *testing.T) {
fakeUserKey, fakeServiceToken, fakeServiceId := "userkey12345", "servicetoken54321", "555000"
fakeMetricKey := "usage[hits]"
authRepInputs := []struct {
name string
userKey, svcId string
auth TokenAuth
extensions map[string]string
Expand Down
4 changes: 2 additions & 2 deletions client/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (client *ThreeScaleClient) Authorize(appId string, serviceToken string, ser
values.Add("service_id", serviceId)

req.URL.RawQuery = values.Encode()
authRepRes, err := client.doHttpReq(req)
authRepRes, err := client.doHttpReq(req, extensions)
if err != nil {
return authRepResp, fmt.Errorf("error calling 3Scale API - %s", err.Error())
}
Expand All @@ -45,7 +45,7 @@ func (client *ThreeScaleClient) AuthorizeKey(userKey string, serviceToken string
values.Add("service_id", serviceId)

req.URL.RawQuery = values.Encode()
resp, err = client.doHttpReq(req)
resp, err = client.doHttpReq(req, extensions)
if err != nil {
return resp, fmt.Errorf("error calling 3Scale API - %s", err.Error())
}
Expand Down
81 changes: 77 additions & 4 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import (
"net/url"
"reflect"
"strconv"
"strings"
)

const (
defaultBackendUrl = "https://su1.3scale.net:443"
queryTag = "query"
defaultBackendUrl = "https://su1.3scale.net:443"
queryTag = "query"
limitExtensions = "limit_headers"
limitRemainingHeaderKey = "3scale-limit-remaining"
limitResetHeaderKey = "3scale-limit-reset"
)

var httpReqError = errors.New("error building http request")
Expand Down Expand Up @@ -52,7 +56,7 @@ func NewThreeScale(backEnd *Backend, httpClient *http.Client) *ThreeScaleClient
}

// GetPeer - a utility method that returns the remote hostname of the client
func (client *ThreeScaleClient) GetPeer() string {
func (client *ThreeScaleClient) GetPeer() string {
return client.backend.host
}

Expand Down Expand Up @@ -93,7 +97,7 @@ func encodeExtensions(extensions map[string]string) string {
}

// Call 3scale backend with the provided HTTP request
func (client *ThreeScaleClient) doHttpReq(req *http.Request) (ApiResponse, error) {
func (client *ThreeScaleClient) doHttpReq(req *http.Request, ext map[string]string) (ApiResponse, error) {
var authRepRes ApiResponse

resp, err := client.httpClient.Do(req)
Expand All @@ -108,10 +112,56 @@ func (client *ThreeScaleClient) doHttpReq(req *http.Request) (ApiResponse, error
if err != nil {
return authRepRes, err
}

authRepRes.StatusCode = resp.StatusCode

if ext != nil {
if _, ok := ext[limitExtensions]; ok {
authRepRes.RateLimits = &RateLimits{}
if limitRem := resp.Header[limitRemainingHeaderKey][0]; limitRem != "" {
remainingLimit, err := strconv.Atoi(limitRem)
if err != nil {
authRepRes.RateLimits = nil
goto out
}
authRepRes.RateLimits.limitRemaining = remainingLimit
}

if limReset := resp.Header[limitResetHeaderKey][0]; limReset != "" {
resetLimit, err := strconv.Atoi(limReset)
if err != nil {
authRepRes.RateLimits = nil
goto out
}
authRepRes.RateLimits.limitReset = resetLimit
}
}
}

out:
return authRepRes, nil
}

// GetLimitRemaining - An integer stating the amount of hits left for the full combination of metrics authorized in this call
// before the rate limiting logic would start denying authorizations for the current period.
// A value of -1 indicates there is no limit in the amount of hits.
// Nil value will indicate the extension has not been used.
func (r RateLimits) GetLimitRemaining() int {
return r.limitRemaining
}

// GetLimitReset - An integer stating the amount of seconds left for the current limiting period to elapse.
// A value of -1 indicates there i is no limit in time.
// Nil value will indicate the extension has not been used.
func (r RateLimits) GetLimitReset() int {
return r.limitReset
}

// GetHierarchy - A list of children (methods) associated with a parent(metric)
func (r ApiResponse) GetHierarchy() map[string][]string {
return r.hierarchy
}

// Add a metric to list of metrics to be reported
// Returns error if provided value is non-positive and entry will be ignored
func (m Metrics) Add(name string, value int) error {
Expand Down Expand Up @@ -179,9 +229,32 @@ func getApiResp(r io.Reader) (ApiResponse, error) {
resp.Reason = apiResp.Code
}
}

if len(apiResp.Hierarchy.Metric) > 0 {
resp.hierarchy = make(map[string][]string, len(apiResp.Hierarchy.Metric))
for _, i := range apiResp.Hierarchy.Metric {
if i.Children != "" {
children := strings.Split(i.Children, " ")
for _, child := range children {
if !contains(child, resp.hierarchy[i.Name]) {
resp.hierarchy[i.Name] = append(resp.hierarchy[i.Name], child)
}
}
}
}
}
return resp, nil
}

func contains(key string, in []string) bool {
for _, i := range in {
if key == i {
return true
}
}
return false
}

// Helper function to read custom tags and add them to query string
// Returns a list of values formatted as expected by 3scale API
func parseQueries(obj interface{}, values url.Values, m Metrics, l Log) url.Values {
Expand Down
106 changes: 98 additions & 8 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package client

import (
"bytes"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"path/filepath"
Expand All @@ -12,6 +14,8 @@ import (
"testing"
"time"
"unsafe"

"github.com/3scale/3scale-go-client/fake"
)

var ext_tested bool
Expand Down Expand Up @@ -83,6 +87,92 @@ func TestNewThreeScale(t *testing.T) {
equals(t, threeScaleTwo.backend, DefaultBackend())
}

// Asserts correct behaviour from response when specific extensions are enabled
func TestExtensions(t *testing.T) {
const empty = ""

tokenAuth := TokenAuth{Type: serviceToken, Value: empty}

inputs := []struct {
name string
extensions map[string]string
xmlResponse string
headers http.Header
// function which should error out or complete if no error detected
isOK func(r ApiResponse, e error)
}{
{
name: "Test Hierarchy Extension",
extensions: map[string]string{"hierarchy": "1"},
xmlResponse: fake.GetHierarchyEnabledResponse(),
isOK: func(r ApiResponse, e error) {
if e != nil {
t.Errorf("expected nil error")
}
if len(r.GetHierarchy()) != 1 {
t.Errorf("expected only one parent in hierarchy")
}
if len(r.GetHierarchy()["hits"]) != 3 {
t.Errorf("expected three children for hits metric")
}

},
headers: make(http.Header),
},
{
name: "Test Limit Extension",
extensions: map[string]string{"limit_headers": "1"},
xmlResponse: fake.GetAuthSuccess(),
isOK: func(r ApiResponse, e error) {
if e != nil {
t.Errorf("expected nil error")
}

if limRem := r.RateLimits.GetLimitRemaining(); limRem != 10 {
t.Errorf("unexpected limit parsing - limit remaining")
}

if limRes := r.RateLimits.GetLimitReset(); limRes != 500 {
t.Errorf("unexpected limit parsing - limit reset")
}
},
headers: http.Header{limitRemainingHeaderKey: []string{"10"}, limitResetHeaderKey: []string{"500"}},
},
}
for _, input := range inputs {
t.Run(input.name, func(t *testing.T) {
httpClient := NewTestClient(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(input.xmlResponse)),
Header: input.headers,
}
})
c := threeScaleTestClient(httpClient)

r, err := c.AuthRepAppID(tokenAuth, empty, empty, AuthRepParams{}, input.extensions)
input.isOK(r, err)

r, err = c.AuthRepUserKey(tokenAuth, empty, empty, AuthRepParams{}, input.extensions)
input.isOK(r, err)

r, err = c.Authorize(empty, empty, empty, AuthorizeParams{}, input.extensions)
input.isOK(r, err)

r, err = c.AuthorizeKey(empty, empty, empty, AuthorizeKeyParams{}, input.extensions)
input.isOK(r, err)

r, err = c.ReportAppID(tokenAuth, empty, ReportTransactions{}, input.extensions)
input.isOK(r, err)

r, err = c.ReportUserKey(tokenAuth, empty, ReportTransactions{}, input.extensions)
input.isOK(r, err)
})

}

}

// Returns a default client for testing
func threeScaleTestClient(hc *http.Client) *ThreeScaleClient {
client := NewThreeScale(DefaultBackend(), hc)
Expand All @@ -104,11 +194,11 @@ func getExtensions(t *testing.T) map[string]string {
// ensure we at least return the extensions the first time we get called
if !ext_tested || rand.Intn(2) != 0 {
ext_tested = true
return map[string]string {
"no_body": "1",
return map[string]string{
"no_body": "1",
"asingle;field": "and;single;value",
"many@@and==": "should@@befine==",
"a test&": "&ok",
"many@@and==": "should@@befine==",
"a test&": "&ok",
}
} else {
return nil
Expand All @@ -118,10 +208,10 @@ func getExtensions(t *testing.T) map[string]string {
// returns a randomly-ordered list of strings for extensions with format "key=value"
func getExtensionsValue() []string {
expected := map[string]string{
"no_body": "1",
"asingle%3Bfield": "and%3Bsingle%3Bvalue",
"no_body": "1",
"asingle%3Bfield": "and%3Bsingle%3Bvalue",
"many%40%40and%3D%3D": "should%40%40befine%3D%3D",
"a+test%26": "%26ok",
"a+test%26": "%26ok",
}

exp := make([]string, 0, unsafe.Sizeof(expected))
Expand All @@ -147,7 +237,7 @@ func checkExtensions(t *testing.T, req *http.Request) (bool, string) {
sort.Strings(expected)
sort.Strings(found)

return false, fmt.Sprintf("\nexpected extension header value %s\n" +
return false, fmt.Sprintf("\nexpected extension header value %s\n"+
" but found %s",
strings.Join(expected, ", "), strings.Join(found, ", "))

Expand Down
2 changes: 1 addition & 1 deletion client/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (client *ThreeScaleClient) report(values url.Values, extensions map[string]
}

req.URL.RawQuery = values.Encode()
resp, err = client.doHttpReq(req)
resp, err = client.doHttpReq(req, extensions)
if err != nil {
return resp, fmt.Errorf("error calling 3Scale API - %s", err.Error())
}
Expand Down
28 changes: 23 additions & 5 deletions client/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ type ApiResponse struct {
Reason string
Success bool
StatusCode int
// nil value indicates 'limit_headers' extension not in use or parsing error with 3scale response.
RateLimits *RateLimits
hierarchy map[string][]string
}

// ApiResponseXML - response from backend API
type ApiResponseXML struct {
Name xml.Name `xml:",any"`
Authorized bool `xml:"authorized,omitempty"`
Reason string `xml:"reason,omitempty"`
Code string `xml:"code,attr,omitempty"`
Name xml.Name `xml:",any"`
Authorized bool `xml:"authorized,omitempty"`
Reason string `xml:"reason,omitempty"`
Code string `xml:"code,attr,omitempty"`
Hierarchy Hierarchy `xml:"hierarchy"`
}

// AuthorizeParams - optional parameters for the Authorize API - App ID pattern
Expand Down Expand Up @@ -80,7 +84,21 @@ type TokenAuth struct {
Value string
}

func (auth *TokenAuth) SetURLValues(values *url.Values) (error) {
// Hierarchy encapsulates the return value when using "hierarchy" extension
type Hierarchy struct {
Metric []struct {
Name string `xml:"name,attr"`
Children string `xml:"children,attr"`
} `xml:"metric"`
}

// RateLimits encapsulates the return values when using the "limit_headers" extension
type RateLimits struct {
limitRemaining int
limitReset int
}

func (auth *TokenAuth) SetURLValues(values *url.Values) error {

switch auth.Type {
case serviceToken:
Expand Down
27 changes: 27 additions & 0 deletions fake/xml_resp.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,30 @@ func GetLimitExceededResp() string {
</usage_reports>
</status>`
}

// Get mock response with hierarchy extension enabled
func GetHierarchyEnabledResponse() string {
return `<?xml version="1.0" encoding="UTF-8"?>
<status>
<authorized>true</authorized>
<plan>Basic</plan>
<usage_reports>
<usage_report metric="hits" period="minute">
<period_start>2019-02-22 14:32:00 +0000</period_start>
<period_end>2019-02-22 14:33:00 +0000</period_end>
<max_value>4</max_value>
<current_value>1</current_value>
</usage_report>
<usage_report metric="test_metric" period="week">
<period_start>2019-02-18 00:00:00 +0000</period_start>
<period_end>2019-02-25 00:00:00 +0000</period_end>
<max_value>6</max_value>
<current_value>0</current_value>
</usage_report>
</usage_reports>
<hierarchy>
<metric name="hits" children="example sample test" />
<metric name="test_metric" children="" />
</hierarchy>
</status>`
}

0 comments on commit e54ff7d

Please sign in to comment.