From 546576657cb31a0441dd86e2340ffcc295da6112 Mon Sep 17 00:00:00 2001 From: zyxkad Date: Tue, 13 Aug 2024 21:58:40 -0600 Subject: [PATCH] add report API --- cluster/cluster.go | 5 ++- cluster/http.go | 10 ++++- cluster/{config.go => requests.go} | 30 +++++++++++++ cluster/storage.go | 35 ++++++--------- utils/encoding.go | 26 +++++++++++ utils/encoding_test.go | 69 ++++++++++++++++++++++++++++++ utils/http.go | 31 ++++++++------ 7 files changed, 170 insertions(+), 36 deletions(-) rename cluster/{config.go => requests.go} (88%) create mode 100644 utils/encoding_test.go diff --git a/cluster/cluster.go b/cluster/cluster.go index 08a850e6..f20416bd 100644 --- a/cluster/cluster.go +++ b/cluster/cluster.go @@ -90,13 +90,14 @@ func NewCluster( } // ID returns the cluster id -// The ID may not be unique in the openbmclapi cluster runtime +// The ID may not be unique in the OpenBMCLAPI cluster runtime. +// To identify the cluster instance for analyzing, use Name instead. func (cr *Cluster) ID() string { return cr.opts.Id } // Name returns the cluster's alias name -// The name must be unique in the openbmclapi cluster runtime +// The name must be unique in the OpenBMCLAPI cluster runtime. func (cr *Cluster) Name() string { return cr.name } diff --git a/cluster/http.go b/cluster/http.go index 07480e33..8c747c3a 100644 --- a/cluster/http.go +++ b/cluster/http.go @@ -115,7 +115,15 @@ func (cr *Cluster) makeReqWithBody( } func (cr *Cluster) makeReqWithAuth(ctx context.Context, method string, relpath string, query url.Values) (req *http.Request, err error) { - req, err = cr.makeReq(ctx, method, relpath, query) + return cr.makeReqWithAuthBody(ctx, method, relpath, query, nil) +} + +func (cr *Cluster) makeReqWithAuthBody( + ctx context.Context, + method string, relpath string, + query url.Values, body io.Reader, +) (req *http.Request, err error) { + req, err = cr.makeReqWithBody(ctx, method, relpath, query, nil) if err != nil { return } diff --git a/cluster/config.go b/cluster/requests.go similarity index 88% rename from cluster/config.go rename to cluster/requests.go index 2eebc617..51209b0f 100644 --- a/cluster/config.go +++ b/cluster/requests.go @@ -259,3 +259,33 @@ func (cr *Cluster) RequestCert(ctx context.Context) (ckp *CertKeyPair, err error } return } + +func (cr *Cluster) ReportDownload(ctx context.Context, request *http.Request, err error) error { + type ReportPayload struct { + Urls []string `json:"urls"` + Error utils.EmbedJSON[struct{ Message string }] `json:"error"` + } + var payload ReportPayload + redirects := utils.GetRedirects(request) + payload.Urls = make([]string, len(redirects)) + for i, u := range redirects { + payload.Urls[i] = u.String() + } + payload.Error.V.Message = err.Error() + data, err := json.Marshal(payload) + if err != nil { + return err + } + req, err := cr.makeReqWithAuthBody(ctx, http.MethodPost, "/openbmclapi/report", nil, bytes.NewReader(data)) + if err != nil { + return err + } + resp, err := cr.client.Do(req) + if err != nil { + return err + } + if resp.StatusCode/100 != 2 { + return utils.NewHTTPStatusErrorFromResponse(resp) + } + return nil +} diff --git a/cluster/storage.go b/cluster/storage.go index 05d45101..35dda611 100644 --- a/cluster/storage.go +++ b/cluster/storage.go @@ -641,29 +641,22 @@ func (c *HTTPClient) fetchFileWithBuf( return } defer res.Body.Close() - if err = ctx.Err(); err != nil { - return - } if res.StatusCode != http.StatusOK { - err = utils.ErrorFromRedirect(utils.NewHTTPStatusErrorFromResponse(res), res) - return - } - switch ce := strings.ToLower(res.Header.Get("Content-Encoding")); ce { - case "": - r = res.Body - case "gzip": - if r, err = gzip.NewReader(res.Body); err != nil { - err = utils.ErrorFromRedirect(err, res) - return + err = utils.NewHTTPStatusErrorFromResponse(res) + }else { + switch ce := strings.ToLower(res.Header.Get("Content-Encoding")); ce { + case "": + r = res.Body + case "gzip": + r, err = gzip.NewReader(res.Body) + case "deflate": + r, err = zlib.NewReader(res.Body) + default: + err = fmt.Errorf("Unexpected Content-Encoding %q", ce) } - case "deflate": - if r, err = zlib.NewReader(res.Body); err != nil { - err = utils.ErrorFromRedirect(err, res) - return - } - default: - err = utils.ErrorFromRedirect(fmt.Errorf("Unexpected Content-Encoding %q", ce), res) - return + } + if err != nil { + return "", utils.ErrorFromRedirect(err, res) } if wrapper != nil { r = wrapper(r) diff --git a/utils/encoding.go b/utils/encoding.go index 2db3b4ca..620c2e50 100644 --- a/utils/encoding.go +++ b/utils/encoding.go @@ -20,6 +20,7 @@ package utils import ( + "encoding/json" "time" "gopkg.in/yaml.v3" @@ -107,3 +108,28 @@ func (d *YAMLDuration) UnmarshalYAML(n *yaml.Node) (err error) { *d = (YAMLDuration)(td) return nil } + +type EmbedJSON[T any] struct { + V T +} + +var ( + _ json.Marshaler = EmbedJSON[any]{} + _ json.Unmarshaler = (*EmbedJSON[any])(nil) +) + +func (e EmbedJSON[T]) MarshalJSON() ([]byte, error) { + data, err := json.Marshal(e.V) + if err != nil { + return nil, err + } + return json.Marshal((string)(data)) +} + +func (e *EmbedJSON[T]) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + return json.Unmarshal(([]byte)(str), &e.V) +} diff --git a/utils/encoding_test.go b/utils/encoding_test.go new file mode 100644 index 00000000..50b3b9e4 --- /dev/null +++ b/utils/encoding_test.go @@ -0,0 +1,69 @@ +/** + * OpenBmclAPI (Golang Edition) + * Copyright (C) 2024 Kevin Z + * All rights reserved + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils_test + +import ( + "testing" + + "encoding/json" + "reflect" + + "github.com/LiterMC/go-openbmclapi/utils" +) + +func TestEmbedJSON(t *testing.T) { + type testPayload struct { + A int + B utils.EmbedJSON[struct { + C string + D float64 + E utils.EmbedJSON[*string] + F *utils.EmbedJSON[string] + }] + G utils.EmbedJSON[*string] + H *utils.EmbedJSON[string] + I utils.EmbedJSON[*string] `json:",omitempty"` + J *utils.EmbedJSON[string] `json:",omitempty"` + } + var v testPayload + v.A = 1 + v.B.V.C = "2\"" + v.B.V.D = 3.4 + v.B.V.E.V = new(string) + *v.B.V.E.V = `{5"6"7}` + v.B.V.F = &utils.EmbedJSON[string]{ + V: "f", + } + data, err := json.Marshal(v) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + dataStr := (string)(data) + if want := `{"A":1,"B":"{\"C\":\"2\\\"\",\"D\":3.4,\"E\":\"\\\"{5\\\\\\\"6\\\\\\\"7}\\\"\",\"F\":\"\\\"f\\\"\"}","G":"null","H":null,"I":"null"}`; dataStr != want { + t.Fatalf("Marshal error, got %s, want %s", dataStr, want) + } + var w testPayload + if err := json.Unmarshal(data, &w); err != nil { + t.Fatalf("Unmarshal error: %v", err) + } + if !reflect.DeepEqual(w, v) { + t.Fatalf("Unmarshal error, got %#v, want %#v", w, v) + } +} diff --git a/utils/http.go b/utils/http.go index 4c6e5e66..25ddfe15 100644 --- a/utils/http.go +++ b/utils/http.go @@ -506,30 +506,37 @@ func (c *connHeadReader) Read(buf []byte) (n int, err error) { return c.Conn.Read(buf) } +func GetRedirects(req *http.Request) []*url.URL { + redirects := make([]*url.URL, 0, 5) + for req != nil { + redirects = append(redirects, req.URL) + resp := req.Response + if resp == nil { + break + } + req = resp.Request + } + if len(redirects) == 0 { + return nil + } + slices.Reverse(redirects) + return redirects +} + type RedirectError struct { Redirects []*url.URL Err error } func ErrorFromRedirect(err error, resp *http.Response) *RedirectError { - redirects := make([]*url.URL, 0, 4) - for resp != nil && resp.Request != nil { - redirects = append(redirects, resp.Request.URL) - resp = resp.Request.Response - } - if len(redirects) > 1 { - slices.Reverse(redirects) - } else { - redirects = nil - } return &RedirectError{ - Redirects: redirects, + Redirects: GetRedirects(resp.Request), Err: err, } } func (e *RedirectError) Error() string { - if len(e.Redirects) == 0 { + if len(e.Redirects) <= 1 { return e.Err.Error() }