// 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 ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) type Client struct { baseURL string } func NewClient(url string) *Client { return &Client{baseURL: strings.TrimRight(url, "/")} } func (client Client) GetSessionSeries(ctx context.Context, sessionID string) (*Series, error) { return getJSON[Series](ctx, client.baseURL+"/sessions/"+sessionID+"/series") } func (client Client) GetSeries(ctx context.Context, seriesID string) (*Series, error) { return getJSON[Series](ctx, client.baseURL+"/series/"+seriesID) } type UploadTriageResultReq struct { SkipReason string `json:"skip_reason"` Log []byte `json:"log"` } func (client Client) UploadTriageResult(ctx context.Context, sessionID string, req *UploadTriageResultReq) error { _, err := postJSON[UploadTriageResultReq, any](ctx, client.baseURL+"/sessions/"+sessionID+"/triage_result", req) return err } type TreesResp struct { Trees []*Tree `json:"trees"` FuzzTargets []*FuzzTriageTarget `json:"fuzz_targets"` } func (client Client) GetTrees(ctx context.Context) (*TreesResp, error) { return getJSON[TreesResp](ctx, client.baseURL+"/trees") } type LastBuildReq struct { Arch string ConfigName string TreeName string Commit string Status string } const BuildSuccess = "success" func (client Client) LastBuild(ctx context.Context, req *LastBuildReq) (*Build, error) { return postJSON[LastBuildReq, Build](ctx, client.baseURL+"/builds/last", req) } type UploadBuildReq struct { Build Config []byte `json:"config"` Log []byte `json:"log"` } type UploadBuildResp struct { ID string } func (client Client) UploadBuild(ctx context.Context, req *UploadBuildReq) (*UploadBuildResp, error) { return postJSON[UploadBuildReq, UploadBuildResp](ctx, client.baseURL+"/builds/upload", req) } func (client Client) UploadTestResult(ctx context.Context, req *TestResult) error { _, err := postJSON[TestResult, any](ctx, client.baseURL+"/tests/upload", req) return err } func (client Client) UploadTestArtifacts(ctx context.Context, sessionID, testName string, tarGzContent io.Reader) error { v := url.Values{} v.Add("session", sessionID) v.Add("test", testName) url := client.baseURL + "/tests/upload_artifacts?" + v.Encode() _, err := postMultiPartFile[any](ctx, url, tarGzContent) return err } func (client Client) UploadFinding(ctx context.Context, req *NewFinding) error { _, err := postJSON[NewFinding, any](ctx, client.baseURL+"/findings/upload", req) return err } type UploadSeriesResp struct { ID string `json:"id"` Saved bool `json:"saved"` } func (client Client) UploadSeries(ctx context.Context, req *Series) (*UploadSeriesResp, error) { return postJSON[Series, UploadSeriesResp](ctx, client.baseURL+"/series/upload", req) } type UploadSessionResp struct { ID string `json:"id"` } func (client Client) UploadSession(ctx context.Context, req *NewSession) (*UploadSessionResp, error) { return postJSON[NewSession, UploadSessionResp](ctx, client.baseURL+"/sessions/upload", req) } type BaseFindingInfo struct { BuildID string `json:"buildID"` Title string `json:"title"` } func (client Client) UploadBaseFinding(ctx context.Context, req *BaseFindingInfo) error { _, err := postJSON[BaseFindingInfo, any](ctx, client.baseURL+"/base_findings/upload", req) return err } type BaseFindingStatus struct { Observed bool `json:"observed"` } func (client Client) BaseFindingStatus(ctx context.Context, req *BaseFindingInfo) (*BaseFindingStatus, error) { return postJSON[BaseFindingInfo, BaseFindingStatus](ctx, client.baseURL+"/base_findings/status", req) } const requestTimeout = time.Minute func finishRequest[Resp any](httpReq *http.Request) (*Resp, error) { client := &http.Client{ Timeout: requestTimeout, } resp, err := client.Do(httpReq) if err != nil { return nil, err } defer resp.Body.Close() if err := ensure200(resp); err != nil { return nil, err } var data Resp err = json.NewDecoder(resp.Body).Decode(&data) if err != nil { return nil, err } return &data, nil } func ensure200(resp *http.Response) error { if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) if len(bodyBytes) > 128 { bodyBytes = bodyBytes[:128] } return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, bodyBytes) } return nil }