diff --git a/example/tokenauth/main.go b/example/tokenauth/main.go index 7f172965f3b..67064fa3616 100644 --- a/example/tokenauth/main.go +++ b/example/tokenauth/main.go @@ -22,6 +22,7 @@ import ( func main() { fmt.Print("GitHub Token: ") byteToken, _ := terminal.ReadPassword(int(syscall.Stdin)) + println() token := string(byteToken) ctx := context.Background() @@ -39,7 +40,12 @@ func main() { } // Rate.Limit should most likely be 5000 when authorized. - log.Printf("Rate: %#v", resp.Rate) + log.Printf("Rate: %#v\n", resp.Rate) + + // If a Token Expiration has been set, it will be displayed. + if !resp.TokenExpiration.IsZero() { + log.Printf("Token Expiration: %v\n", resp.TokenExpiration) + } fmt.Printf("\n%v\n", github.Stringify(user)) } diff --git a/github/github.go b/github/github.go index 2f7653654ff..fe686857ad9 100644 --- a/github/github.go +++ b/github/github.go @@ -37,6 +37,8 @@ const ( headerRateReset = "X-RateLimit-Reset" headerOTP = "X-GitHub-OTP" + headerTokenExpiration = "GitHub-Authentication-Token-Expiration" + mediaTypeV3 = "application/vnd.github.v3+json" defaultMediaType = "application/octet-stream" mediaTypeV3SHA = "application/vnd.github.v3.sha" @@ -464,6 +466,9 @@ type Response struct { // Explicitly specify the Rate type so Rate's String() receiver doesn't // propagate to Response. Rate Rate + + // token's expiration date + TokenExpiration Timestamp } // newResponse creates a new Response for the provided http.Response. @@ -472,6 +477,7 @@ func newResponse(r *http.Response) *Response { response := &Response{Response: r} response.populatePageValues() response.Rate = parseRate(r) + response.TokenExpiration = parseTokenExpiration(r) return response } @@ -551,6 +557,17 @@ func parseRate(r *http.Response) Rate { return rate } +// parseTokenExpiration parses the TokenExpiration related headers. +func parseTokenExpiration(r *http.Response) Timestamp { + var exp Timestamp + if v := r.Header.Get(headerTokenExpiration); v != "" { + if t, err := time.Parse("2006-01-02 03:04:05 MST", v); err == nil { + exp = Timestamp{t.Local()} + } + } + return exp +} + type requestContext uint8 const ( diff --git a/github/github_test.go b/github/github_test.go index d2240558da5..666bb45ca18 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -2206,3 +2206,36 @@ func TestRateLimits_Marshal(t *testing.T) { testJSONMarshal(t, u, want) } + +func TestParseTokenExpiration(t *testing.T) { + tests := []struct { + header string + want Timestamp + }{ + { + header: "", + want: Timestamp{}, + }, + { + header: "this is a garbage", + want: Timestamp{}, + }, + { + header: "2021-09-03 02:34:04 UTC", + want: Timestamp{time.Date(2021, time.September, 3, 2, 34, 4, 0, time.UTC)}, + }, + } + + for _, tt := range tests { + res := &http.Response{ + Request: &http.Request{}, + Header: http.Header{}, + } + + res.Header.Set(headerTokenExpiration, tt.header) + exp := parseTokenExpiration(res) + if !exp.Equal(tt.want) { + t.Errorf("parseTokenExpiration returned %#v, want %#v", exp, tt.want) + } + } +}