aboutsummaryrefslogtreecommitdiffstats
path: root/dashboard
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2024-10-11 13:17:50 +0200
committerDmitry Vyukov <dvyukov@google.com>2024-10-15 08:34:09 +0000
commitb9fb2ea80e9852ae6465fcb3b822ca9ffa3306bd (patch)
tree6ef60254c3832271607cd34823308104ed8284cf /dashboard
parentbea340a021473d87bbbd5da6e88e575cc5e71cb0 (diff)
dashboard/api: add Client type
Diffstat (limited to 'dashboard')
-rw-r--r--dashboard/api/api.go2
-rw-r--r--dashboard/api/client.go142
-rw-r--r--dashboard/app/public_json_api.go6
-rw-r--r--dashboard/app/public_json_api_test.go28
-rw-r--r--dashboard/app/util_test.go34
5 files changed, 196 insertions, 16 deletions
diff --git a/dashboard/api/api.go b/dashboard/api/api.go
index ec5e5a508..09dcf7690 100644
--- a/dashboard/api/api.go
+++ b/dashboard/api/api.go
@@ -5,6 +5,8 @@
// All structures in this package are backwards compatible.
package api
+const Version = 1
+
type BugGroup struct {
Version int `json:"version"`
Bugs []BugSummary
diff --git a/dashboard/api/client.go b/dashboard/api/client.go
new file mode 100644
index 000000000..b2ad3a3d4
--- /dev/null
+++ b/dashboard/api/client.go
@@ -0,0 +1,142 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "html"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+)
+
+type Client struct {
+ url string
+ token string
+ throttle bool
+ ctor requestCtor
+ doer requestDoer
+}
+
+// accessToken is OAuth access token obtained with "gcloud auth print-access-token"
+// (provided your account has at least user level access to the dashboard).
+// If the token is provided, dashboard should disable API throttling.
+// The token can be empty, in which case the dashboard may throttle requests.
+func NewClient(dashboardURL, accessToken string) *Client {
+ return &Client{
+ url: strings.TrimSuffix(dashboardURL, "/"),
+ token: accessToken,
+ throttle: true,
+ ctor: http.NewRequest,
+ doer: http.DefaultClient.Do,
+ }
+}
+
+type (
+ requestCtor func(method, url string, body io.Reader) (*http.Request, error)
+ requestDoer func(req *http.Request) (*http.Response, error)
+)
+
+func NewTestClient(ctor requestCtor, doer requestDoer) *Client {
+ return &Client{
+ url: "http://localhost",
+ ctor: ctor,
+ doer: doer,
+ }
+}
+
+type BugGroupType int
+
+const (
+ BugGroupOpen BugGroupType = 1 << iota
+ BugGroupFixed
+ BugGroupInvalid
+ BugGroupAll = ^0
+)
+
+var groupSuffix = map[BugGroupType]string{
+ BugGroupFixed: "/fixed",
+ BugGroupInvalid: "/invalid",
+}
+
+func (c *Client) BugGroups(ns string, groups BugGroupType) ([]BugSummary, error) {
+ var bugs []BugSummary
+ for _, typ := range []BugGroupType{BugGroupOpen, BugGroupFixed, BugGroupInvalid} {
+ if (groups & typ) == 0 {
+ continue
+ }
+ url := "/" + ns + groupSuffix[typ]
+ var group BugGroup
+ if err := c.query(url, &group); err != nil {
+ return nil, err
+ }
+ bugs = append(bugs, group.Bugs...)
+ }
+ return bugs, nil
+}
+
+func (c *Client) Bug(link string) (*Bug, error) {
+ bug := new(Bug)
+ return bug, c.query(link, bug)
+}
+
+func (c *Client) Text(query string) ([]byte, error) {
+ queryURL, err := c.queryURL(query)
+ if err != nil {
+ return nil, err
+ }
+ req, err := c.ctor(http.MethodGet, queryURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("http.NewRequest: %w", err)
+ }
+ if c.token != "" {
+ req.Header.Add("Authorization", "Bearer "+c.token)
+ } else if c.throttle {
+ <-throttler
+ }
+ res, err := c.doer(req)
+ if err != nil {
+ return nil, fmt.Errorf("http.Get(%v): %w", queryURL, err)
+ }
+ defer res.Body.Close()
+ body, err := io.ReadAll(res.Body)
+ if res.StatusCode < 200 || res.StatusCode >= 300 || err != nil {
+ return nil, fmt.Errorf("api request %q failed: %v (%w)", queryURL, res.StatusCode, err)
+ }
+ return body, nil
+}
+
+func (c *Client) query(query string, result any) error {
+ data, err := c.Text(query)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(data, result); err != nil {
+ return fmt.Errorf("json.Unmarshal: %w\n%s", err, data)
+ }
+ if ver := reflect.ValueOf(result).Elem().FieldByName("Version").Int(); ver != Version {
+ return fmt.Errorf("unsupported export version %v (expect %v)", ver, Version)
+ }
+ return nil
+}
+
+func (c *Client) queryURL(query string) (string, error) {
+ // All links in API are html escaped for some reason, unescape them.
+ query = c.url + html.UnescapeString(query)
+ u, err := url.Parse(query)
+ if err != nil {
+ return "", fmt.Errorf("url.Parse(%v): %w", query, err)
+ }
+ vals := u.Query()
+ // json=1 is ignored for text end points, so we don't bother not adding it.
+ vals.Set("json", "1")
+ u.RawQuery = vals.Encode()
+ return u.String(), nil
+}
+
+var throttler = time.NewTicker(time.Second).C
diff --git a/dashboard/app/public_json_api.go b/dashboard/app/public_json_api.go
index 05d0de441..ce1c674be 100644
--- a/dashboard/app/public_json_api.go
+++ b/dashboard/app/public_json_api.go
@@ -11,7 +11,7 @@ import (
func getExtAPIDescrForBugPage(bugPage *uiBugPage) *api.Bug {
return &api.Bug{
- Version: 1,
+ Version: api.Version,
Title: bugPage.Bug.Title,
ID: bugPage.Bug.ID,
Discussions: func() []string {
@@ -81,7 +81,7 @@ func getExtAPIDescrForBugGroups(bugGroups []*uiBugGroup) *api.BugGroup {
}
}
return &api.BugGroup{
- Version: 1,
+ Version: api.Version,
Bugs: bugs,
}
}
@@ -115,7 +115,7 @@ type publicAPIBackports struct {
func getExtAPIDescrForBackports(groups []*uiBackportGroup) *publicAPIBackports {
return &publicAPIBackports{
- Version: 1,
+ Version: api.Version,
List: func() []publicMissingBackport {
var res []publicMissingBackport
for _, group := range groups {
diff --git a/dashboard/app/public_json_api_test.go b/dashboard/app/public_json_api_test.go
index ebf943438..82cf9c320 100644
--- a/dashboard/app/public_json_api_test.go
+++ b/dashboard/app/public_json_api_test.go
@@ -7,6 +7,7 @@ import (
"fmt"
"testing"
+ "github.com/google/syzkaller/dashboard/api"
"github.com/google/syzkaller/dashboard/dashapi"
)
@@ -222,3 +223,30 @@ func TestJSONAPICauseBisection(t *testing.T) {
]
}`)
}
+
+func TestPublicJSONAPI(t *testing.T) {
+ c := NewCtx(t)
+ defer c.Close()
+
+ client := c.makeClient(clientPublic, keyPublic, true)
+ build := testBuild(1)
+ client.UploadBuild(build)
+ client.ReportCrash(testCrashWithRepro(build, 1))
+ rep := client.pollBug()
+ client.updateBug(rep.ID, dashapi.BugStatusUpstream, "")
+ _ = client.pollBug()
+
+ cli := c.makeAPIClient()
+ bugs, err := cli.BugGroups("access-public", api.BugGroupAll)
+ c.expectOK(err)
+ c.expectEQ(len(bugs), 1)
+ c.expectEQ(bugs[0].Title, "title1")
+
+ bug, err := cli.Bug(bugs[0].Link)
+ c.expectOK(err)
+ c.expectEQ(bug.Title, "title1")
+
+ config, err := cli.Text(bug.Crashes[0].KernelConfigLink)
+ c.expectOK(err)
+ c.expectEQ(config, []byte("config1"))
+}
diff --git a/dashboard/app/util_test.go b/dashboard/app/util_test.go
index f30581095..f865ff612 100644
--- a/dashboard/app/util_test.go
+++ b/dashboard/app/util_test.go
@@ -26,6 +26,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
+ "github.com/google/syzkaller/dashboard/api"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"github.com/google/syzkaller/pkg/subsystem"
@@ -496,18 +497,6 @@ type apiClient struct {
}
func (c *Ctx) makeClient(client, key string, failOnErrors bool) *apiClient {
- doer := func(r *http.Request) (*http.Response, error) {
- r = registerRequest(r, c)
- r = r.WithContext(c.transformContext(r.Context()))
- w := httptest.NewRecorder()
- http.DefaultServeMux.ServeHTTP(w, r)
- res := &http.Response{
- StatusCode: w.Code,
- Status: http.StatusText(w.Code),
- Body: io.NopCloser(w.Result().Body),
- }
- return res, nil
- }
logger := func(msg string, args ...interface{}) {
c.t.Logf("%v: "+msg, append([]interface{}{caller(3)}, args...)...)
}
@@ -516,7 +505,7 @@ func (c *Ctx) makeClient(client, key string, failOnErrors bool) *apiClient {
c.t.Fatalf("\n%v: %v", caller(2), err)
}
}
- dash, err := dashapi.NewCustom(client, "", key, c.inst.NewRequest, doer, logger, errorHandler)
+ dash, err := dashapi.NewCustom(client, "", key, c.inst.NewRequest, c.httpDoer(), logger, errorHandler)
if err != nil {
panic(fmt.Sprintf("Impossible error: %v", err))
}
@@ -526,6 +515,25 @@ func (c *Ctx) makeClient(client, key string, failOnErrors bool) *apiClient {
}
}
+func (c *Ctx) makeAPIClient() *api.Client {
+ return api.NewTestClient(c.inst.NewRequest, c.httpDoer())
+}
+
+func (c *Ctx) httpDoer() func(*http.Request) (*http.Response, error) {
+ return func(r *http.Request) (*http.Response, error) {
+ r = registerRequest(r, c)
+ r = r.WithContext(c.transformContext(r.Context()))
+ w := httptest.NewRecorder()
+ http.DefaultServeMux.ServeHTTP(w, r)
+ res := &http.Response{
+ StatusCode: w.Code,
+ Status: http.StatusText(w.Code),
+ Body: io.NopCloser(w.Result().Body),
+ }
+ return res, nil
+ }
+}
+
func (client *apiClient) pollBugs(expect int) []*dashapi.BugReport {
resp, _ := client.ReportingPollBugs("test")
if len(resp.Reports) != expect {