github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/dashboard/api/client.go (about) 1 // Copyright 2024 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package api 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "html" 10 "io" 11 "net/http" 12 "net/url" 13 "reflect" 14 "strings" 15 "time" 16 ) 17 18 type Client struct { 19 url string 20 token string 21 throttle bool 22 ctor requestCtor 23 doer requestDoer 24 } 25 26 // accessToken is OAuth access token obtained with "gcloud auth print-access-token" 27 // (provided your account has at least user level access to the dashboard). 28 // If the token is provided, dashboard should disable API throttling. 29 // The token can be empty, in which case the dashboard may throttle requests. 30 func NewClient(dashboardURL, accessToken string) *Client { 31 return &Client{ 32 url: strings.TrimSuffix(dashboardURL, "/"), 33 token: accessToken, 34 throttle: true, 35 ctor: http.NewRequest, 36 doer: http.DefaultClient.Do, 37 } 38 } 39 40 type ( 41 requestCtor func(method, url string, body io.Reader) (*http.Request, error) 42 requestDoer func(req *http.Request) (*http.Response, error) 43 ) 44 45 func NewTestClient(ctor requestCtor, doer requestDoer) *Client { 46 return &Client{ 47 url: "http://localhost", 48 ctor: ctor, 49 doer: doer, 50 } 51 } 52 53 type BugGroupType int 54 55 const ( 56 BugGroupOpen BugGroupType = 1 << iota 57 BugGroupFixed 58 BugGroupInvalid 59 BugGroupAll = ^0 60 ) 61 62 var groupSuffix = map[BugGroupType]string{ 63 BugGroupFixed: "/fixed", 64 BugGroupInvalid: "/invalid", 65 } 66 67 func (c *Client) BugGroups(ns string, groups BugGroupType) ([]BugSummary, error) { 68 var bugs []BugSummary 69 for _, typ := range []BugGroupType{BugGroupOpen, BugGroupFixed, BugGroupInvalid} { 70 if (groups & typ) == 0 { 71 continue 72 } 73 url := "/" + ns + groupSuffix[typ] 74 var group BugGroup 75 if err := c.query(url, &group); err != nil { 76 return nil, err 77 } 78 bugs = append(bugs, group.Bugs...) 79 } 80 return bugs, nil 81 } 82 83 func (c *Client) Bug(link string) (*Bug, error) { 84 bug := new(Bug) 85 return bug, c.query(link, bug) 86 } 87 88 func (c *Client) Text(query string) ([]byte, error) { 89 queryURL, err := c.queryURL(query) 90 if err != nil { 91 return nil, err 92 } 93 req, err := c.ctor(http.MethodGet, queryURL, nil) 94 if err != nil { 95 return nil, fmt.Errorf("http.NewRequest: %w", err) 96 } 97 if c.token != "" { 98 req.Header.Add("Authorization", "Bearer "+c.token) 99 } else if c.throttle { 100 <-throttler 101 } 102 res, err := c.doer(req) 103 if err != nil { 104 return nil, fmt.Errorf("http.Get(%v): %w", queryURL, err) 105 } 106 defer res.Body.Close() 107 body, err := io.ReadAll(res.Body) 108 if res.StatusCode < 200 || res.StatusCode >= 300 || err != nil { 109 return nil, fmt.Errorf("api request %q failed: status(%v) err(%w) body(%.1024s)", 110 queryURL, res.StatusCode, err, string(body)) 111 } 112 return body, nil 113 } 114 115 func (c *Client) query(query string, result any) error { 116 data, err := c.Text(query) 117 if err != nil { 118 return err 119 } 120 if err := json.Unmarshal(data, result); err != nil { 121 return fmt.Errorf("json.Unmarshal: %w\n%s", err, data) 122 } 123 if ver := reflect.ValueOf(result).Elem().FieldByName("Version").Int(); ver != Version { 124 return fmt.Errorf("unsupported export version %v (expect %v)", ver, Version) 125 } 126 return nil 127 } 128 129 func (c *Client) queryURL(query string) (string, error) { 130 // All links in API are html escaped for some reason, unescape them. 131 query = c.url + html.UnescapeString(query) 132 u, err := url.Parse(query) 133 if err != nil { 134 return "", fmt.Errorf("url.Parse(%v): %w", query, err) 135 } 136 vals := u.Query() 137 // json=1 is ignored for text end points, so we don't bother not adding it. 138 vals.Set("json", "1") 139 u.RawQuery = vals.Encode() 140 return u.String(), nil 141 } 142 143 var throttler = time.NewTicker(time.Second).C