forked from bradleyfalzon/ghinstallation
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtransport.go
137 lines (119 loc) · 4.57 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
package ghinstallation
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
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
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"`
}
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
}
func (t *Transport) refreshToken(ctx context.Context) error {
req, err := http.NewRequest("POST", fmt.Sprintf("%s/app/installations/%v/access_tokens", t.BaseURL, t.installationID), nil)
if err != nil {
return fmt.Errorf("could not create request: %s", err)
}
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("received non 2xx response status %q when fetching %v", resp.Status, req.URL)
}
return json.NewDecoder(resp.Body).Decode(&t.token)
}