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