~samwhited/paddle

ref: v0.0.3 paddle/roundtripper.go -rw-r--r-- 2.7 KiB
fa35a6a5Sam Whited paddle: add note about maintenance status 1 year, 5 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package paddle

import (
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

// Version is the Paddle API version that this package supports.
// It is exported for convenience.
const Version = "2.0"

const (
	apiHost = "vendors.paddle.com"
	baseURL = "https://" + apiHost + "/api/" + Version
)

// NewRoundTripper wraps an existing http.RoundTripper so that any POST request
// using content-type application/x-www-form-urlencoded sent through it to
// "https://vendors.paddle.com/api/version" will include authentication
// information.
// If a transport is nil, http.DefaultTransport is used instead.
//
// This is a lower level API exposed for advanced users; most of the time, users
// will want to create a Client and then use it with one of the subpackages
// instead.
func NewRoundTripper(transport http.RoundTripper, vendorID int, vendorAuthCode string) http.RoundTripper {
	if transport == nil {
		transport = http.DefaultTransport
	}
	if rt, ok := transport.(authRoundTripper); ok {
		return rt
	}

	v := url.Values{}
	v.Set("vendor_id", strconv.Itoa(vendorID))
	v.Set("vendor_auth_code", vendorAuthCode)
	encoded := v.Encode()
	return authRoundTripper{
		transport: transport,
		authData:  encoded,
	}
}

type authRoundTripper struct {
	transport http.RoundTripper
	authData  string
}

// RoundTrip fullfills the http.RoundTripper interface for authRoundTripper.
func (rt authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	if req == nil {
		return rt.transport.RoundTrip(req)
	}

	if req.Method == http.MethodPost &&
		req.URL.Scheme == "https" && req.URL.Host == apiHost &&
		strings.HasPrefix(req.URL.Path, "/api/"+Version) &&
		req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
		cr := &checkRead{inner: req.Body}
		va := &valuesAppender{cr: cr, authData: rt.authData}
		req.Body = struct {
			io.Closer
			io.Reader
		}{
			Closer: req.Body,
			Reader: io.MultiReader(cr, va),
		}
	}
	return rt.transport.RoundTrip(req)
}

// checkRead is an io.ReadCloser that sets readSome if any data is ever read
// through it.
// Close calls are proxied.
type checkRead struct {
	inner    io.Reader
	readSome bool
}

func (r *checkRead) Read(p []byte) (int, error) {
	n, err := r.inner.Read(p)
	if n > 0 {
		r.readSome = true
	}

	return n, err
}

// valuesAppender checks if the cr has been read on its first read, and then
// sets up an inner reader that reads authData (prefixed with an "&" if
// required).
type valuesAppender struct {
	cr       *checkRead
	inner    io.Reader
	authData string
}

func (r *valuesAppender) Read(p []byte) (int, error) {
	if r.inner == nil {
		if r.cr.readSome {
			r.inner = strings.NewReader("&" + r.authData)
		} else {
			r.inner = strings.NewReader(r.authData)
		}
	}

	return r.inner.Read(p)
}