aboutsummaryrefslogtreecommitdiff
path: root/pkg/client/client.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/client/client.go')
-rw-r--r--pkg/client/client.go174
1 files changed, 174 insertions, 0 deletions
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
+}