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