1
2
3
4
5
6
7
8
9 package github
10
11 import (
12 "bytes"
13 "context"
14 "encoding/json"
15 "errors"
16 "fmt"
17 "io"
18 "io/ioutil"
19 "net/http"
20 "net/url"
21 "reflect"
22 "strconv"
23 "strings"
24 "sync"
25 "time"
26
27 "github.com/google/go-querystring/query"
28 )
29
30 const (
31 defaultBaseURL = "https://api.github.com/"
32 uploadBaseURL = "https://uploads.github.com/"
33 userAgent = "go-github"
34
35 headerRateLimit = "X-RateLimit-Limit"
36 headerRateRemaining = "X-RateLimit-Remaining"
37 headerRateReset = "X-RateLimit-Reset"
38 headerOTP = "X-GitHub-OTP"
39
40 headerTokenExpiration = "GitHub-Authentication-Token-Expiration"
41
42 mediaTypeV3 = "application/vnd.github.v3+json"
43 defaultMediaType = "application/octet-stream"
44 mediaTypeV3SHA = "application/vnd.github.v3.sha"
45 mediaTypeV3Diff = "application/vnd.github.v3.diff"
46 mediaTypeV3Patch = "application/vnd.github.v3.patch"
47 mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json"
48 mediaTypeIssueImportAPI = "application/vnd.github.golden-comet-preview+json"
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 mediaTypeStarringPreview = "application/vnd.github.v3.star+json"
69
70
71 mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json"
72
73
74 mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json"
75
76
77 mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json"
78
79
80 mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview"
81
82
83 mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json"
84
85
86 mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json"
87
88
89 mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json"
90
91
92 mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json"
93
94
95 mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json"
96
97
98 mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json"
99
100
101 mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json"
102
103
104 mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json"
105
106
107 mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview"
108
109
110 mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json"
111
112
113 mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json"
114
115
116 mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json"
117
118
119 mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json"
120
121
122 mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json"
123
124
125 mediaTypeRequiredAutomatedSecurityFixesPreview = "application/vnd.github.london-preview+json"
126
127
128 mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json"
129
130
131 mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json"
132
133
134 mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json"
135
136
137 mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json"
138
139
140 mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json"
141
142
143 mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json"
144
145
146 mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json"
147
148
149 mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json"
150 )
151
152 var errNonNilContext = errors.New("context must be non-nil")
153
154
155 type Client struct {
156 clientMu sync.Mutex
157 client *http.Client
158
159
160
161
162 BaseURL *url.URL
163
164
165 UploadURL *url.URL
166
167
168 UserAgent string
169
170 rateMu sync.Mutex
171 rateLimits [categories]Rate
172
173 common service
174
175
176 Actions *ActionsService
177 Activity *ActivityService
178 Admin *AdminService
179 Apps *AppsService
180 Authorizations *AuthorizationsService
181 Billing *BillingService
182 Checks *ChecksService
183 CodeScanning *CodeScanningService
184 Dependabot *DependabotService
185 Enterprise *EnterpriseService
186 Gists *GistsService
187 Git *GitService
188 Gitignores *GitignoresService
189 Interactions *InteractionsService
190 IssueImport *IssueImportService
191 Issues *IssuesService
192 Licenses *LicensesService
193 Marketplace *MarketplaceService
194 Migrations *MigrationService
195 Organizations *OrganizationsService
196 Projects *ProjectsService
197 PullRequests *PullRequestsService
198 Reactions *ReactionsService
199 Repositories *RepositoriesService
200 SCIM *SCIMService
201 Search *SearchService
202 SecretScanning *SecretScanningService
203 Teams *TeamsService
204 Users *UsersService
205 }
206
207 type service struct {
208 client *Client
209 }
210
211
212 func (c *Client) Client() *http.Client {
213 c.clientMu.Lock()
214 defer c.clientMu.Unlock()
215 clientCopy := *c.client
216 return &clientCopy
217 }
218
219
220
221 type ListOptions struct {
222
223 Page int `url:"page,omitempty"`
224
225
226 PerPage int `url:"per_page,omitempty"`
227 }
228
229
230
231 type ListCursorOptions struct {
232
233 Page string `url:"page,omitempty"`
234
235
236 PerPage int `url:"per_page,omitempty"`
237
238
239 After string `url:"after,omitempty"`
240
241
242 Before string `url:"before,omitempty"`
243
244
245 Cursor string `url:"cursor,omitempty"`
246 }
247
248
249 type UploadOptions struct {
250 Name string `url:"name,omitempty"`
251 Label string `url:"label,omitempty"`
252 MediaType string `url:"-"`
253 }
254
255
256 type RawType uint8
257
258 const (
259
260 Diff RawType = 1 + iota
261
262 Patch
263 )
264
265
266
267 type RawOptions struct {
268 Type RawType
269 }
270
271
272
273 func addOptions(s string, opts interface{}) (string, error) {
274 v := reflect.ValueOf(opts)
275 if v.Kind() == reflect.Ptr && v.IsNil() {
276 return s, nil
277 }
278
279 u, err := url.Parse(s)
280 if err != nil {
281 return s, err
282 }
283
284 qs, err := query.Values(opts)
285 if err != nil {
286 return s, err
287 }
288
289 u.RawQuery = qs.Encode()
290 return u.String(), nil
291 }
292
293
294
295
296
297 func NewClient(httpClient *http.Client) *Client {
298 if httpClient == nil {
299 httpClient = &http.Client{}
300 }
301 baseURL, _ := url.Parse(defaultBaseURL)
302 uploadURL, _ := url.Parse(uploadBaseURL)
303
304 c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent, UploadURL: uploadURL}
305 c.common.client = c
306 c.Actions = (*ActionsService)(&c.common)
307 c.Activity = (*ActivityService)(&c.common)
308 c.Admin = (*AdminService)(&c.common)
309 c.Apps = (*AppsService)(&c.common)
310 c.Authorizations = (*AuthorizationsService)(&c.common)
311 c.Billing = (*BillingService)(&c.common)
312 c.Checks = (*ChecksService)(&c.common)
313 c.CodeScanning = (*CodeScanningService)(&c.common)
314 c.Dependabot = (*DependabotService)(&c.common)
315 c.Enterprise = (*EnterpriseService)(&c.common)
316 c.Gists = (*GistsService)(&c.common)
317 c.Git = (*GitService)(&c.common)
318 c.Gitignores = (*GitignoresService)(&c.common)
319 c.Interactions = (*InteractionsService)(&c.common)
320 c.IssueImport = (*IssueImportService)(&c.common)
321 c.Issues = (*IssuesService)(&c.common)
322 c.Licenses = (*LicensesService)(&c.common)
323 c.Marketplace = &MarketplaceService{client: c}
324 c.Migrations = (*MigrationService)(&c.common)
325 c.Organizations = (*OrganizationsService)(&c.common)
326 c.Projects = (*ProjectsService)(&c.common)
327 c.PullRequests = (*PullRequestsService)(&c.common)
328 c.Reactions = (*ReactionsService)(&c.common)
329 c.Repositories = (*RepositoriesService)(&c.common)
330 c.SCIM = (*SCIMService)(&c.common)
331 c.Search = (*SearchService)(&c.common)
332 c.SecretScanning = (*SecretScanningService)(&c.common)
333 c.Teams = (*TeamsService)(&c.common)
334 c.Users = (*UsersService)(&c.common)
335 return c
336 }
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351 func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) {
352 baseEndpoint, err := url.Parse(baseURL)
353 if err != nil {
354 return nil, err
355 }
356
357 if !strings.HasSuffix(baseEndpoint.Path, "/") {
358 baseEndpoint.Path += "/"
359 }
360 if !strings.HasSuffix(baseEndpoint.Path, "/api/v3/") &&
361 !strings.HasPrefix(baseEndpoint.Host, "api.") &&
362 !strings.Contains(baseEndpoint.Host, ".api.") {
363 baseEndpoint.Path += "api/v3/"
364 }
365
366 uploadEndpoint, err := url.Parse(uploadURL)
367 if err != nil {
368 return nil, err
369 }
370
371 if !strings.HasSuffix(uploadEndpoint.Path, "/") {
372 uploadEndpoint.Path += "/"
373 }
374 if !strings.HasSuffix(uploadEndpoint.Path, "/api/uploads/") &&
375 !strings.HasPrefix(uploadEndpoint.Host, "api.") &&
376 !strings.Contains(uploadEndpoint.Host, ".api.") {
377 uploadEndpoint.Path += "api/uploads/"
378 }
379
380 c := NewClient(httpClient)
381 c.BaseURL = baseEndpoint
382 c.UploadURL = uploadEndpoint
383 return c, nil
384 }
385
386
387
388
389
390
391 func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
392 if !strings.HasSuffix(c.BaseURL.Path, "/") {
393 return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
394 }
395
396 u, err := c.BaseURL.Parse(urlStr)
397 if err != nil {
398 return nil, err
399 }
400
401 var buf io.ReadWriter
402 if body != nil {
403 buf = &bytes.Buffer{}
404 enc := json.NewEncoder(buf)
405 enc.SetEscapeHTML(false)
406 err := enc.Encode(body)
407 if err != nil {
408 return nil, err
409 }
410 }
411
412 req, err := http.NewRequest(method, u.String(), buf)
413 if err != nil {
414 return nil, err
415 }
416
417 if body != nil {
418 req.Header.Set("Content-Type", "application/json")
419 }
420 req.Header.Set("Accept", mediaTypeV3)
421 if c.UserAgent != "" {
422 req.Header.Set("User-Agent", c.UserAgent)
423 }
424 return req, nil
425 }
426
427
428
429
430 func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) {
431 if !strings.HasSuffix(c.UploadURL.Path, "/") {
432 return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL)
433 }
434 u, err := c.UploadURL.Parse(urlStr)
435 if err != nil {
436 return nil, err
437 }
438
439 req, err := http.NewRequest("POST", u.String(), reader)
440 if err != nil {
441 return nil, err
442 }
443
444 req.ContentLength = size
445
446 if mediaType == "" {
447 mediaType = defaultMediaType
448 }
449 req.Header.Set("Content-Type", mediaType)
450 req.Header.Set("Accept", mediaTypeV3)
451 req.Header.Set("User-Agent", c.UserAgent)
452 return req, nil
453 }
454
455
456
457
458 type Response struct {
459 *http.Response
460
461
462
463
464
465
466
467
468 NextPage int
469 PrevPage int
470 FirstPage int
471 LastPage int
472
473
474
475
476
477
478
479
480
481
482
483
484 NextPageToken string
485
486
487
488
489 Cursor string
490
491
492 Before string
493 After string
494
495
496
497 Rate Rate
498
499
500 TokenExpiration Timestamp
501 }
502
503
504
505 func newResponse(r *http.Response) *Response {
506 response := &Response{Response: r}
507 response.populatePageValues()
508 response.Rate = parseRate(r)
509 response.TokenExpiration = parseTokenExpiration(r)
510 return response
511 }
512
513
514
515 func (r *Response) populatePageValues() {
516 if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 {
517 for _, link := range strings.Split(links[0], ",") {
518 segments := strings.Split(strings.TrimSpace(link), ";")
519
520
521 if len(segments) < 2 {
522 continue
523 }
524
525
526 if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") {
527 continue
528 }
529
530
531 url, err := url.Parse(segments[0][1 : len(segments[0])-1])
532 if err != nil {
533 continue
534 }
535
536 q := url.Query()
537
538 if cursor := q.Get("cursor"); cursor != "" {
539 for _, segment := range segments[1:] {
540 switch strings.TrimSpace(segment) {
541 case `rel="next"`:
542 r.Cursor = cursor
543 }
544 }
545
546 continue
547 }
548
549 page := q.Get("page")
550 since := q.Get("since")
551 before := q.Get("before")
552 after := q.Get("after")
553
554 if page == "" && before == "" && after == "" && since == "" {
555 continue
556 }
557
558 if since != "" && page == "" {
559 page = since
560 }
561
562 for _, segment := range segments[1:] {
563 switch strings.TrimSpace(segment) {
564 case `rel="next"`:
565 if r.NextPage, err = strconv.Atoi(page); err != nil {
566 r.NextPageToken = page
567 }
568 r.After = after
569 case `rel="prev"`:
570 r.PrevPage, _ = strconv.Atoi(page)
571 r.Before = before
572 case `rel="first"`:
573 r.FirstPage, _ = strconv.Atoi(page)
574 case `rel="last"`:
575 r.LastPage, _ = strconv.Atoi(page)
576 }
577 }
578 }
579 }
580 }
581
582
583 func parseRate(r *http.Response) Rate {
584 var rate Rate
585 if limit := r.Header.Get(headerRateLimit); limit != "" {
586 rate.Limit, _ = strconv.Atoi(limit)
587 }
588 if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
589 rate.Remaining, _ = strconv.Atoi(remaining)
590 }
591 if reset := r.Header.Get(headerRateReset); reset != "" {
592 if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
593 rate.Reset = Timestamp{time.Unix(v, 0)}
594 }
595 }
596 return rate
597 }
598
599
600 func parseTokenExpiration(r *http.Response) Timestamp {
601 var exp Timestamp
602 if v := r.Header.Get(headerTokenExpiration); v != "" {
603 if t, err := time.Parse("2006-01-02 03:04:05 MST", v); err == nil {
604 exp = Timestamp{t.Local()}
605 }
606 }
607 return exp
608 }
609
610 type requestContext uint8
611
612 const (
613 bypassRateLimitCheck requestContext = iota
614 )
615
616
617
618
619
620
621
622
623
624 func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, error) {
625 if ctx == nil {
626 return nil, errNonNilContext
627 }
628
629 req = withContext(ctx, req)
630
631 rateLimitCategory := category(req.URL.Path)
632
633 if bypass := ctx.Value(bypassRateLimitCheck); bypass == nil {
634
635 if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
636 return &Response{
637 Response: err.Response,
638 Rate: err.Rate,
639 }, err
640 }
641 }
642
643 resp, err := c.client.Do(req)
644 if err != nil {
645
646
647 select {
648 case <-ctx.Done():
649 return nil, ctx.Err()
650 default:
651 }
652
653
654 if e, ok := err.(*url.Error); ok {
655 if url, err := url.Parse(e.URL); err == nil {
656 e.URL = sanitizeURL(url).String()
657 return nil, e
658 }
659 }
660
661 return nil, err
662 }
663
664 response := newResponse(resp)
665
666
667
668 if response.Header.Get("X-From-Cache") == "" {
669 c.rateMu.Lock()
670 c.rateLimits[rateLimitCategory] = response.Rate
671 c.rateMu.Unlock()
672 }
673
674 err = CheckResponse(resp)
675 if err != nil {
676 defer resp.Body.Close()
677
678
679
680
681
682 aerr, ok := err.(*AcceptedError)
683 if ok {
684 b, readErr := ioutil.ReadAll(resp.Body)
685 if readErr != nil {
686 return response, readErr
687 }
688
689 aerr.Raw = b
690 err = aerr
691 }
692 }
693 return response, err
694 }
695
696
697
698
699
700
701
702
703
704
705
706 func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
707 resp, err := c.BareDo(ctx, req)
708 if err != nil {
709 return resp, err
710 }
711 defer resp.Body.Close()
712
713 switch v := v.(type) {
714 case nil:
715 case io.Writer:
716 _, err = io.Copy(v, resp.Body)
717 default:
718 decErr := json.NewDecoder(resp.Body).Decode(v)
719 if decErr == io.EOF {
720 decErr = nil
721 }
722 if decErr != nil {
723 err = decErr
724 }
725 }
726 return resp, err
727 }
728
729
730
731
732
733 func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rateLimitCategory) *RateLimitError {
734 c.rateMu.Lock()
735 rate := c.rateLimits[rateLimitCategory]
736 c.rateMu.Unlock()
737 if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) {
738
739 resp := &http.Response{
740 Status: http.StatusText(http.StatusForbidden),
741 StatusCode: http.StatusForbidden,
742 Request: req,
743 Header: make(http.Header),
744 Body: ioutil.NopCloser(strings.NewReader("")),
745 }
746 return &RateLimitError{
747 Rate: rate,
748 Response: resp,
749 Message: fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time),
750 }
751 }
752
753 return nil
754 }
755
756
757
758
759 func compareHTTPResponse(r1, r2 *http.Response) bool {
760 if r1 == nil && r2 == nil {
761 return true
762 }
763
764 if r1 != nil && r2 != nil {
765 return r1.StatusCode == r2.StatusCode
766 }
767 return false
768 }
769
770
775 type ErrorResponse struct {
776 Response *http.Response
777 Message string `json:"message"`
778 Errors []Error `json:"errors"`
779
780 Block *ErrorBlock `json:"block,omitempty"`
781
782
783
784 DocumentationURL string `json:"documentation_url,omitempty"`
785 }
786
787
788
789
790 type ErrorBlock struct {
791 Reason string `json:"reason,omitempty"`
792 CreatedAt *Timestamp `json:"created_at,omitempty"`
793 }
794
795 func (r *ErrorResponse) Error() string {
796 return fmt.Sprintf("%v %v: %d %v %+v",
797 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
798 r.Response.StatusCode, r.Message, r.Errors)
799 }
800
801
802 func (r *ErrorResponse) Is(target error) bool {
803 v, ok := target.(*ErrorResponse)
804 if !ok {
805 return false
806 }
807
808 if r.Message != v.Message || (r.DocumentationURL != v.DocumentationURL) ||
809 !compareHTTPResponse(r.Response, v.Response) {
810 return false
811 }
812
813
814 if len(r.Errors) != len(v.Errors) {
815 return false
816 }
817 for idx := range r.Errors {
818 if r.Errors[idx] != v.Errors[idx] {
819 return false
820 }
821 }
822
823
824 if (r.Block != nil && v.Block == nil) || (r.Block == nil && v.Block != nil) {
825 return false
826 }
827 if r.Block != nil && v.Block != nil {
828 if r.Block.Reason != v.Block.Reason {
829 return false
830 }
831 if (r.Block.CreatedAt != nil && v.Block.CreatedAt == nil) || (r.Block.CreatedAt ==
832 nil && v.Block.CreatedAt != nil) {
833 return false
834 }
835 if r.Block.CreatedAt != nil && v.Block.CreatedAt != nil {
836 if *(r.Block.CreatedAt) != *(v.Block.CreatedAt) {
837 return false
838 }
839 }
840 }
841
842 return true
843 }
844
845
846
847
848 type TwoFactorAuthError ErrorResponse
849
850 func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() }
851
852
853
854 type RateLimitError struct {
855 Rate Rate
856 Response *http.Response
857 Message string `json:"message"`
858 }
859
860 func (r *RateLimitError) Error() string {
861 return fmt.Sprintf("%v %v: %d %v %v",
862 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
863 r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time)))
864 }
865
866
867 func (r *RateLimitError) Is(target error) bool {
868 v, ok := target.(*RateLimitError)
869 if !ok {
870 return false
871 }
872
873 return r.Rate == v.Rate &&
874 r.Message == v.Message &&
875 compareHTTPResponse(r.Response, v.Response)
876 }
877
878
879
880
881
882
883
884 type AcceptedError struct {
885
886 Raw []byte
887 }
888
889 func (*AcceptedError) Error() string {
890 return "job scheduled on GitHub side; try again later"
891 }
892
893
894 func (ae *AcceptedError) Is(target error) bool {
895 v, ok := target.(*AcceptedError)
896 if !ok {
897 return false
898 }
899 return bytes.Compare(ae.Raw, v.Raw) == 0
900 }
901
902
903
904 type AbuseRateLimitError struct {
905 Response *http.Response
906 Message string `json:"message"`
907
908
909
910
911 RetryAfter *time.Duration
912 }
913
914 func (r *AbuseRateLimitError) Error() string {
915 return fmt.Sprintf("%v %v: %d %v",
916 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
917 r.Response.StatusCode, r.Message)
918 }
919
920
921 func (r *AbuseRateLimitError) Is(target error) bool {
922 v, ok := target.(*AbuseRateLimitError)
923 if !ok {
924 return false
925 }
926
927 return r.Message == v.Message &&
928 r.RetryAfter == v.RetryAfter &&
929 compareHTTPResponse(r.Response, v.Response)
930 }
931
932
933
934 func sanitizeURL(uri *url.URL) *url.URL {
935 if uri == nil {
936 return nil
937 }
938 params := uri.Query()
939 if len(params.Get("client_secret")) > 0 {
940 params.Set("client_secret", "REDACTED")
941 uri.RawQuery = params.Encode()
942 }
943 return uri
944 }
945
946
968 type Error struct {
969 Resource string `json:"resource"`
970 Field string `json:"field"`
971 Code string `json:"code"`
972 Message string `json:"message"`
973 }
974
975 func (e *Error) Error() string {
976 return fmt.Sprintf("%v error caused by %v field on %v resource",
977 e.Code, e.Field, e.Resource)
978 }
979
980 func (e *Error) UnmarshalJSON(data []byte) error {
981 type aliasError Error
982 if err := json.Unmarshal(data, (*aliasError)(e)); err != nil {
983 return json.Unmarshal(data, &e.Message)
984 }
985 return nil
986 }
987
988
989
990
991
992
993
994
995
996
997 func CheckResponse(r *http.Response) error {
998 if r.StatusCode == http.StatusAccepted {
999 return &AcceptedError{}
1000 }
1001 if c := r.StatusCode; 200 <= c && c <= 299 {
1002 return nil
1003 }
1004
1005 errorResponse := &ErrorResponse{Response: r}
1006 data, err := ioutil.ReadAll(r.Body)
1007 if err == nil && data != nil {
1008 json.Unmarshal(data, errorResponse)
1009 }
1010
1011
1012
1013 r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
1014 switch {
1015 case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"):
1016 return (*TwoFactorAuthError)(errorResponse)
1017 case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0":
1018 return &RateLimitError{
1019 Rate: parseRate(r),
1020 Response: errorResponse.Response,
1021 Message: errorResponse.Message,
1022 }
1023 case r.StatusCode == http.StatusForbidden &&
1024 (strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") ||
1025 strings.HasSuffix(errorResponse.DocumentationURL, "#secondary-rate-limits")):
1026 abuseRateLimitError := &AbuseRateLimitError{
1027 Response: errorResponse.Response,
1028 Message: errorResponse.Message,
1029 }
1030 if v := r.Header["Retry-After"]; len(v) > 0 {
1031
1032
1033
1034 retryAfterSeconds, _ := strconv.ParseInt(v[0], 10, 64)
1035 retryAfter := time.Duration(retryAfterSeconds) * time.Second
1036 abuseRateLimitError.RetryAfter = &retryAfter
1037 }
1038 return abuseRateLimitError
1039 default:
1040 return errorResponse
1041 }
1042 }
1043
1044
1045
1046
1047
1048
1049 func parseBoolResponse(err error) (bool, error) {
1050 if err == nil {
1051 return true, nil
1052 }
1053
1054 if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound {
1055
1056 return false, nil
1057 }
1058
1059
1060 return false, err
1061 }
1062
1063
1064 type Rate struct {
1065
1066 Limit int `json:"limit"`
1067
1068
1069 Remaining int `json:"remaining"`
1070
1071
1072 Reset Timestamp `json:"reset"`
1073 }
1074
1075 func (r Rate) String() string {
1076 return Stringify(r)
1077 }
1078
1079
1080 type RateLimits struct {
1081
1082
1083
1084
1085
1086 Core *Rate `json:"core"`
1087
1088
1089
1090
1091
1092
1093 Search *Rate `json:"search"`
1094
1095
1096 GraphQL *Rate `json:"graphql"`
1097
1098
1099 IntegrationManifest *Rate `json:"integration_manifest"`
1100
1101 SourceImport *Rate `json:"source_import"`
1102 CodeScanningUpload *Rate `json:"code_scanning_upload"`
1103 ActionsRunnerRegistration *Rate `json:"actions_runner_registration"`
1104 SCIM *Rate `json:"scim"`
1105 }
1106
1107 func (r RateLimits) String() string {
1108 return Stringify(r)
1109 }
1110
1111 type rateLimitCategory uint8
1112
1113 const (
1114 coreCategory rateLimitCategory = iota
1115 searchCategory
1116 graphqlCategory
1117 integrationManifestCategory
1118 sourceImportCategory
1119 codeScanningUploadCategory
1120 actionsRunnerRegistrationCategory
1121 scimCategory
1122
1123 categories
1124 )
1125
1126
1127 func category(path string) rateLimitCategory {
1128 switch {
1129 default:
1130 return coreCategory
1131 case strings.HasPrefix(path, "/search/"):
1132 return searchCategory
1133 }
1134 }
1135
1136
1137 func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) {
1138 req, err := c.NewRequest("GET", "rate_limit", nil)
1139 if err != nil {
1140 return nil, nil, err
1141 }
1142
1143 response := new(struct {
1144 Resources *RateLimits `json:"resources"`
1145 })
1146
1147
1148 ctx = context.WithValue(ctx, bypassRateLimitCheck, true)
1149 resp, err := c.Do(ctx, req, response)
1150 if err != nil {
1151 return nil, resp, err
1152 }
1153
1154 if response.Resources != nil {
1155 c.rateMu.Lock()
1156 if response.Resources.Core != nil {
1157 c.rateLimits[coreCategory] = *response.Resources.Core
1158 }
1159 if response.Resources.Search != nil {
1160 c.rateLimits[searchCategory] = *response.Resources.Search
1161 }
1162 if response.Resources.GraphQL != nil {
1163 c.rateLimits[graphqlCategory] = *response.Resources.GraphQL
1164 }
1165 if response.Resources.IntegrationManifest != nil {
1166 c.rateLimits[integrationManifestCategory] = *response.Resources.IntegrationManifest
1167 }
1168 if response.Resources.SourceImport != nil {
1169 c.rateLimits[sourceImportCategory] = *response.Resources.SourceImport
1170 }
1171 if response.Resources.CodeScanningUpload != nil {
1172 c.rateLimits[codeScanningUploadCategory] = *response.Resources.CodeScanningUpload
1173 }
1174 if response.Resources.ActionsRunnerRegistration != nil {
1175 c.rateLimits[actionsRunnerRegistrationCategory] = *response.Resources.ActionsRunnerRegistration
1176 }
1177 if response.Resources.SCIM != nil {
1178 c.rateLimits[scimCategory] = *response.Resources.SCIM
1179 }
1180 c.rateMu.Unlock()
1181 }
1182
1183 return response.Resources, resp, nil
1184 }
1185
1186 func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request {
1187
1188
1189
1190
1191
1192
1193 convertedRequest := new(http.Request)
1194 *convertedRequest = *req
1195 convertedRequest.Header = make(http.Header, len(req.Header))
1196
1197 for k, s := range req.Header {
1198 convertedRequest.Header[k] = append([]string(nil), s...)
1199 }
1200 convertedRequest.SetBasicAuth(id, secret)
1201 return convertedRequest
1202 }
1203
1204
1220 type UnauthenticatedRateLimitedTransport struct {
1221
1222
1223
1224 ClientID string
1225
1226
1227
1228 ClientSecret string
1229
1230
1231
1232 Transport http.RoundTripper
1233 }
1234
1235
1236 func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
1237 if t.ClientID == "" {
1238 return nil, errors.New("t.ClientID is empty")
1239 }
1240 if t.ClientSecret == "" {
1241 return nil, errors.New("t.ClientSecret is empty")
1242 }
1243
1244 req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret)
1245
1246 return t.transport().RoundTrip(req2)
1247 }
1248
1249
1250
1251 func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client {
1252 return &http.Client{Transport: t}
1253 }
1254
1255 func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper {
1256 if t.Transport != nil {
1257 return t.Transport
1258 }
1259 return http.DefaultTransport
1260 }
1261
1262
1263
1264
1265
1266 type BasicAuthTransport struct {
1267 Username string
1268 Password string
1269 OTP string
1270
1271
1272
1273 Transport http.RoundTripper
1274 }
1275
1276
1277 func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
1278 req2 := setCredentialsAsHeaders(req, t.Username, t.Password)
1279 if t.OTP != "" {
1280 req2.Header.Set(headerOTP, t.OTP)
1281 }
1282 return t.transport().RoundTrip(req2)
1283 }
1284
1285
1286
1287 func (t *BasicAuthTransport) Client() *http.Client {
1288 return &http.Client{Transport: t}
1289 }
1290
1291 func (t *BasicAuthTransport) transport() http.RoundTripper {
1292 if t.Transport != nil {
1293 return t.Transport
1294 }
1295 return http.DefaultTransport
1296 }
1297
1298
1299
1300
1301 func formatRateReset(d time.Duration) string {
1302 isNegative := d < 0
1303 if isNegative {
1304 d *= -1
1305 }
1306 secondsTotal := int(0.5 + d.Seconds())
1307 minutes := secondsTotal / 60
1308 seconds := secondsTotal - minutes*60
1309
1310 var timeString string
1311 if minutes > 0 {
1312 timeString = fmt.Sprintf("%dm%02ds", minutes, seconds)
1313 } else {
1314 timeString = fmt.Sprintf("%ds", seconds)
1315 }
1316
1317 if isNegative {
1318 return fmt.Sprintf("[rate limit was reset %v ago]", timeString)
1319 }
1320 return fmt.Sprintf("[rate reset in %v]", timeString)
1321 }
1322
1323
1324
1325 func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
1326 req, err := c.NewRequest("GET", u, nil)
1327 if err != nil {
1328 return nil, err
1329 }
1330
1331 var resp *http.Response
1332
1333 req = withContext(ctx, req)
1334 if c.client.Transport == nil {
1335 resp, err = http.DefaultTransport.RoundTrip(req)
1336 } else {
1337 resp, err = c.client.Transport.RoundTrip(req)
1338 }
1339 if err != nil {
1340 return nil, err
1341 }
1342
1343
1344 if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
1345 resp.Body.Close()
1346 u = resp.Header.Get("Location")
1347 resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false)
1348 }
1349 return resp, err
1350 }
1351
1352
1353
1354 func Bool(v bool) *bool { return &v }
1355
1356
1357
1358 func Int(v int) *int { return &v }
1359
1360
1361
1362 func Int64(v int64) *int64 { return &v }
1363
1364
1365
1366 func String(v string) *string { return &v }
1367
View as plain text