-
Notifications
You must be signed in to change notification settings - Fork 101
/
Copy pathtransport.go
186 lines (162 loc) · 6.18 KB
/
transport.go
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package ghinstallation
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"sync"
"time"
"github.com/google/go-github/github"
)
const (
// acceptHeader is the GitHub Apps Preview Accept header.
acceptHeader = "application/vnd.github.machine-man-preview+json"
apiBaseURL = "https://api.github.com"
)
// Transport provides a http.RoundTripper by wrapping an existing
// http.RoundTripper and provides GitHub Apps authentication as an
// installation.
//
// Client can also be overwritten, and is useful to change to one which
// provides retry logic if you do experience retryable errors.
//
// See https://developer.github.com/apps/building-integrations/setting-up-and-registering-github-apps/about-authentication-options-for-github-apps/
type Transport struct {
BaseURL string // BaseURL is the scheme and host for GitHub API, defaults to https://api.github.com
Client Client // Client to use to refresh tokens, defaults to http.Client with provided transport
tr http.RoundTripper // tr is the underlying roundtripper being wrapped
appID int // appID is the GitHub App's ID
installationID int // installationID is the GitHub App Installation ID
InstallationTokenOptions *github.InstallationTokenOptions // parameters restrict a token's access
appsTransport *AppsTransport
mu *sync.Mutex // mu protects token
token *accessToken // token is the installation's access token
}
// accessToken is an installation access token response from GitHub
type accessToken struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
Permissions github.InstallationPermissions `json:"permissions,omitempty"`
Repositories []github.Repository `json:"repositories,omitempty"`
}
var _ http.RoundTripper = &Transport{}
// NewKeyFromFile returns a Transport using a private key from file.
func NewKeyFromFile(tr http.RoundTripper, appID, installationID int, privateKeyFile string) (*Transport, error) {
privateKey, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
return nil, fmt.Errorf("could not read private key: %s", err)
}
return New(tr, appID, installationID, privateKey)
}
// Client is a HTTP client which sends a http.Request and returns a http.Response
// or an error.
type Client interface {
Do(*http.Request) (*http.Response, error)
}
// New returns an Transport using private key. The key is parsed
// and if any errors occur the error is non-nil.
//
// The provided tr http.RoundTripper should be shared between multiple
// installations to ensure reuse of underlying TCP connections.
//
// The returned Transport's RoundTrip method is safe to be used concurrently.
func New(tr http.RoundTripper, appID, installationID int, privateKey []byte) (*Transport, error) {
t := &Transport{
tr: tr,
appID: appID,
installationID: installationID,
BaseURL: apiBaseURL,
Client: &http.Client{Transport: tr},
mu: &sync.Mutex{},
}
var err error
t.appsTransport, err = NewAppsTransport(t.tr, t.appID, privateKey)
if err != nil {
return nil, err
}
return t, nil
}
// RoundTrip implements http.RoundTripper interface.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
token, err := t.Token(req.Context())
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "token "+token)
req.Header.Add("Accept", acceptHeader) // We add to "Accept" header to avoid overwriting existing req headers.
resp, err := t.tr.RoundTrip(req)
return resp, err
}
// Token checks the active token expiration and renews if necessary. Token returns
// a valid access token. If renewal fails an error is returned.
func (t *Transport) Token(ctx context.Context) (string, error) {
t.mu.Lock()
defer t.mu.Unlock()
if t.token == nil || t.token.ExpiresAt.Add(-time.Minute).Before(time.Now()) {
// Token is not set or expired/nearly expired, so refresh
if err := t.refreshToken(ctx); err != nil {
return "", fmt.Errorf("could not refresh installation id %v's token: %s", t.installationID, err)
}
}
return t.token.Token, nil
}
// Permissions returns a transport token's GitHub installation permissions.
func (t *Transport) Permissions() (github.InstallationPermissions, error) {
if t.token == nil {
return github.InstallationPermissions{}, fmt.Errorf("Permissions() = nil, err: nil token")
}
return t.token.Permissions, nil
}
// Repositories returns a transport token's GitHub repositories.
func (t *Transport) Repositories() ([]github.Repository, error) {
if t.token == nil {
return nil, fmt.Errorf("Repositories() = nil, err: nil token")
}
return t.token.Repositories, nil
}
func (t *Transport) refreshToken(ctx context.Context) error {
// Convert InstallationTokenOptions into a ReadWriter to pass as an argument to http.NewRequest.
body, err := GetReadWriter(t.InstallationTokenOptions)
if err != nil {
return fmt.Errorf("could not convert installation token parameters into json: %s", err)
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/app/installations/%v/access_tokens", t.BaseURL, t.installationID), body)
if err != nil {
return fmt.Errorf("could not create request: %s", err)
}
// Set Content and Accept headers.
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", acceptHeader)
if ctx != nil {
req = req.WithContext(ctx)
}
t.appsTransport.BaseURL = t.BaseURL
t.appsTransport.Client = t.Client
resp, err := t.appsTransport.RoundTrip(req)
if err != nil {
return fmt.Errorf("could not get access_tokens from GitHub API for installation ID %v: %v", t.installationID, err)
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("request %+v received non 2xx response status %q with body %+v and TLS %+v", resp.Request, resp.Body, resp.Request, resp.TLS)
}
return json.NewDecoder(resp.Body).Decode(&t.token)
}
// GetReadWriter converts a body interface into an io.ReadWriter object.
func GetReadWriter(i interface{}) (io.ReadWriter, error) {
var buf io.ReadWriter
if i != nil {
buf = new(bytes.Buffer)
enc := json.NewEncoder(buf)
err := enc.Encode(i)
if err != nil {
return nil, err
}
}
return buf, nil
}