From f28d4c28fc553506e01bee3488eaa772e683194b Mon Sep 17 00:00:00 2001 From: Linus Nordberg Date: Tue, 14 Jun 2022 10:56:26 +0200 Subject: add start of a client package To be improved and populated further. It is good enough to help us forward while prototyping primary-secondary log-go setup. --- pkg/client/client.go | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 pkg/client/client.go (limited to 'pkg/client/client.go') diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..ec49f92 --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,174 @@ +package client + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "time" + + "git.sigsum.org/sigsum-go/pkg/requests" + "git.sigsum.org/sigsum-go/pkg/types" + "git.sigsum.org/sigsum-go/pkg/log" + "git.sigsum.org/sigsum-go/pkg/merkle" +) + +type Client interface { + GetUnsignedTreeHead(context.Context) (types.TreeHead, error) + GetToCosignTreeHead(context.Context) (types.SignedTreeHead, error) + GetCosignedTreeHead(context.Context) (types.CosignedTreeHead, error) + GetInclusionProof(context.Context, requests.InclusionProof) (types.InclusionProof, error) + GetConsistencyProof(context.Context, requests.ConsistencyProof) (types.ConsistencyProof, error) + GetLeaves(context.Context, requests.Leaves) (types.Leaves, error) + + AddLeaf(context.Context, requests.Leaf) (bool, error) + AddCosignature(context.Context, requests.Cosignature) error + + Initiated() bool +} + +type Config struct { + UserAgent string + LogURL string + LogPub types.PublicKey + // TODO: witness public keys + policy +} + +func New(cfg Config) Client { + return &client{ + Config: cfg, + Client: http.Client{}, + } +} + +type client struct { + Config + http.Client +} + +func (cli *client) Initiated() bool { + return cli.LogURL != "" +} + +func (cli *client) GetUnsignedTreeHead(ctx context.Context) (th types.TreeHead, err error) { + body, _, err := cli.get(ctx, types.EndpointGetTreeHeadUnsigned.Path(cli.LogURL)) + if err != nil { + return th, fmt.Errorf("get: %w", err) + } + if err := th.FromASCII(bytes.NewBuffer(body)); err != nil { + return th, fmt.Errorf("parse: %w", err) + } + + return th, nil +} + +func (cli *client) GetToCosignTreeHead(ctx context.Context) (sth types.SignedTreeHead, err error) { + body, _, err := cli.get(ctx, types.EndpointGetTreeHeadToCosign.Path(cli.LogURL)) + if err != nil { + return sth, fmt.Errorf("get: %w", err) + } + if err := sth.FromASCII(bytes.NewBuffer(body)); err != nil { + return sth, fmt.Errorf("parse: %w", err) + } + if ok := sth.Verify(&cli.LogPub, merkle.HashFn(cli.LogPub[:])); !ok { + return sth, fmt.Errorf("invalid log signature") + } + + return sth, nil +} + +func (cli *client) GetCosignedTreeHead(ctx context.Context) (cth types.CosignedTreeHead, err error) { + body, _, err := cli.get(ctx, types.EndpointGetTreeHeadCosigned.Path(cli.LogURL)) + if err != nil { + return cth, fmt.Errorf("get: %w", err) + } + if err := cth.FromASCII(bytes.NewBuffer(body)); err != nil { + return cth, fmt.Errorf("parse: %w", err) + } + if ok := cth.SignedTreeHead.Verify(&cli.LogPub, merkle.HashFn(cli.LogPub[:])); !ok { + return cth, fmt.Errorf("invalid log signature") + } + // TODO: verify cosignatures based on policy + return cth, nil +} + +func (cli *client) GetInclusionProof(ctx context.Context, req requests.InclusionProof) (proof types.InclusionProof, err error) { + return proof, fmt.Errorf("TODO") +} + +func (cli *client) GetConsistencyProof(ctx context.Context, req requests.ConsistencyProof) (proof types.ConsistencyProof, err error) { + body, _, err := cli.get(ctx, req.ToURL(types.EndpointGetConsistencyProof.Path(cli.LogURL))) + if err != nil { + return proof, fmt.Errorf("get: %w", err) + } + if err := proof.FromASCII(bytes.NewBuffer(body), req.OldSize, req.NewSize); err != nil { + return proof, fmt.Errorf("parse: %w", err) + } + return proof, nil +} + +func (cli *client) GetLeaves(ctx context.Context, req requests.Leaves) (leaves types.Leaves, err error) { + body, _, err := cli.get(ctx, req.ToURL(types.EndpointGetLeaves.Path(cli.LogURL))) + if err != nil { + return leaves, fmt.Errorf("get: %w", err) + } + if err := leaves.FromASCII(bytes.NewBuffer(body)); err != nil { + return leaves, fmt.Errorf("parse: %w", err) + } + return leaves, nil +} + +func (cli *client) AddLeaf(ctx context.Context, req requests.Leaf) (persisted bool, err error) { + return false, fmt.Errorf("TODO") +} + +func (cli *client) AddCosignature(ctx context.Context, req requests.Cosignature) error { + return fmt.Errorf("TODO") +} + +func (cli *client) get(ctx context.Context, url string) ([]byte, int, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, -1, err + } + return cli.do(ctx, req) +} + +func (cli *client) post(ctx context.Context, url string, body []byte) ([]byte, int, error) { + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body)) + if err != nil { + return nil, -1, err + } + return cli.do(ctx, req) +} + +func (cli *client) do(ctx context.Context, req *http.Request) ([]byte, int, error) { + // TODO: redirects, see go doc http.Client.CheckRedirect + // TODO: use ctx or remove it -- the context is already set on req so it seems unneccesary + req.Header.Set("User-Agent", cli.UserAgent) + + var rsp *http.Response + var err error + for wait := 1; wait < 10 ; wait *= 2 { + log.Debug("trying %v", req.URL) + if rsp, err = cli.Client.Do(req); err == nil { + break + } + sleep := time.Duration(wait) * time.Second + log.Debug("retrying in %v", sleep) + time.Sleep(sleep) + } + if err != nil { + return nil, -1, fmt.Errorf("send request: %w", err) + } + defer rsp.Body.Close() + b, err := ioutil.ReadAll(rsp.Body) + if err != nil { + return nil, rsp.StatusCode, fmt.Errorf("read response: %w", err) + } + if low, high := 200, 299; rsp.StatusCode < low || rsp.StatusCode > high { + err = fmt.Errorf("not 2XX status code: %d", rsp.StatusCode) + } + return b, rsp.StatusCode, err +} -- cgit v1.2.3