aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Nordberg <linus@nordberg.se>2022-06-14 10:56:26 +0200
committerRasmus Dahlberg <rasmus@mullvad.net>2022-06-21 19:46:54 +0200
commitf28d4c28fc553506e01bee3488eaa772e683194b (patch)
tree4885592e8fde581457f89510a29a595a145cf676
parente12a479eeeb787786db0acc54150c05cc7034397 (diff)
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.
-rw-r--r--pkg/client/client.go174
-rw-r--r--pkg/client/client_test.go53
2 files changed, 227 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
+}
diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go
new file mode 100644
index 0000000..98c29c9
--- /dev/null
+++ b/pkg/client/client_test.go
@@ -0,0 +1,53 @@
+package client
+
+//import (
+// "context"
+// "time"
+//
+// "git.sigsum.org/sigsum-go/internal/fmtio"
+// "git.sigsum.org/sigsum-go/pkg/log"
+// "git.sigsum.org/sigsum-go/pkg/requests"
+//)
+//
+//const (
+// //logURL = "https://poc.sigsum.org/crocodile-icefish/sigsum/v0"
+// logURL = "http://localhost:4711/crocodile-icefish/sigsum/v0"
+// logPublicKey = "4791eff3bfc17f352bcc76d4752b38c07882093a5935a84577c63de224b0f6b3"
+// userAgent = "example agent"
+//)
+//
+//func Example() {
+// log.SetLevel(log.DebugLevel)
+// ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+// defer cancel()
+//
+// logPub, err := fmtio.PublicKeyFromHex(logPublicKey)
+// if err != nil {
+// log.Fatal("%s", err.Error())
+// }
+// cli := New(Config{
+// UserAgent: userAgent,
+// LogURL: logURL,
+// LogPub: logPub,
+// })
+//
+// cth, err := cli.GetCosignedTreeHead(ctx)
+// if err != nil {
+// log.Fatal("%s", err.Error())
+// }
+//
+// log.Debug("tree size is %d", cth.TreeSize)
+//
+// leaves, err := cli.GetLeaves(ctx, requests.Leaves{0, cth.TreeSize})
+// if err != nil {
+// log.Fatal("%s", err.Error())
+// }
+//
+// for i, leaf := range leaves {
+// log.Debug("leaf %d has key hash %x", i, leaf.KeyHash[:])
+// }
+//
+// log.Debug("repeat get-leaves call from index %d to get more leaves", len(leaves))
+//
+// // Output:
+//}