aboutsummaryrefslogtreecommitdiff
path: root/pkg/instance
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/instance')
-rw-r--r--pkg/instance/endpoint.go122
-rw-r--r--pkg/instance/endpoint_test.go480
-rw-r--r--pkg/instance/instance.go90
-rw-r--r--pkg/instance/instance_test.go158
-rw-r--r--pkg/instance/metric.go23
-rw-r--r--pkg/instance/request.go77
-rw-r--r--pkg/instance/request_test.go318
7 files changed, 1268 insertions, 0 deletions
diff --git a/pkg/instance/endpoint.go b/pkg/instance/endpoint.go
new file mode 100644
index 0000000..5085c49
--- /dev/null
+++ b/pkg/instance/endpoint.go
@@ -0,0 +1,122 @@
+package stfe
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/golang/glog"
+)
+
+func addLeaf(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
+ glog.V(3).Info("handling add-entry request")
+ req, err := i.leafRequestFromHTTP(r)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ if err := i.Client.AddLeaf(ctx, req); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func addCosignature(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
+ glog.V(3).Info("handling add-cosignature request")
+ req, err := i.cosignatureRequestFromHTTP(r)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ vk := i.Witnesses[*req.KeyHash]
+ if err := i.Stateman.AddCosignature(ctx, &vk, req.Signature); err != nil {
+ return http.StatusBadRequest, err
+ }
+ return http.StatusOK, nil
+}
+
+func getTreeHeadLatest(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) {
+ glog.V(3).Info("handling get-tree-head-latest request")
+ sth, err := i.Stateman.Latest(ctx)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := sth.MarshalASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getTreeHeadToSign(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) {
+ glog.V(3).Info("handling get-tree-head-to-sign request")
+ sth, err := i.Stateman.ToSign(ctx)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := sth.MarshalASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getTreeHeadCosigned(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) {
+ glog.V(3).Info("handling get-tree-head-cosigned request")
+ sth, err := i.Stateman.Cosigned(ctx)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := sth.MarshalASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getConsistencyProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
+ glog.V(3).Info("handling get-consistency-proof request")
+ req, err := i.consistencyProofRequestFromHTTP(r)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ proof, err := i.Client.GetConsistencyProof(ctx, req)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := proof.MarshalASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getInclusionProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
+ glog.V(3).Info("handling get-proof-by-hash request")
+ req, err := i.inclusionProofRequestFromHTTP(r)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ proof, err := i.Client.GetInclusionProof(ctx, req)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if err := proof.MarshalASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
+
+func getLeaves(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
+ glog.V(3).Info("handling get-leaves request")
+ req, err := i.leavesRequestFromHTTP(r)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ leaves, err := i.Client.GetLeaves(ctx, req)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ for _, leaf := range *leaves {
+ if err := leaf.MarshalASCII(w); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ }
+ return http.StatusOK, nil
+}
diff --git a/pkg/instance/endpoint_test.go b/pkg/instance/endpoint_test.go
new file mode 100644
index 0000000..8511b8d
--- /dev/null
+++ b/pkg/instance/endpoint_test.go
@@ -0,0 +1,480 @@
+package stfe
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ cttestdata "github.com/google/certificate-transparency-go/trillian/testdata"
+ "github.com/google/trillian"
+ "github.com/system-transparency/stfe/pkg/testdata"
+ "github.com/system-transparency/stfe/pkg/types"
+)
+
+func TestEndpointAddEntry(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ breq *bytes.Buffer
+ trsp *trillian.QueueLeafResponse
+ terr error
+ wantCode int
+ }{
+ {
+ description: "invalid: bad request: empty",
+ breq: bytes.NewBuffer(nil),
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ description: "invalid: bad Trillian response: error",
+ breq: testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter, testdata.Ed25519VkSubmitter),
+ terr: fmt.Errorf("backend failure"),
+ wantCode: http.StatusInternalServerError,
+ },
+ {
+ description: "valid",
+ breq: testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter, testdata.Ed25519VkSubmitter),
+ trsp: testdata.DefaultTQlr(t, false),
+ wantCode: http.StatusOK,
+ },
+ } {
+ func() { // run deferred functions at the end of each iteration
+ ti := newTestInstance(t, nil)
+ defer ti.ctrl.Finish()
+
+ url := EndpointAddEntry.Path("http://example.com", ti.instance.LogParameters.Prefix)
+ req, err := http.NewRequest("POST", url, table.breq)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+ if table.trsp != nil || table.terr != nil {
+ ti.client.EXPECT().QueueLeaf(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr)
+ }
+
+ w := httptest.NewRecorder()
+ ti.postHandler(t, EndpointAddEntry).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+func TestEndpointAddCosignature(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ breq *bytes.Buffer
+ wantCode int
+ }{
+ {
+ description: "invalid: bad request: empty",
+ breq: bytes.NewBuffer(nil),
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ description: "invalid: signed wrong sth", // newLogParameters() use testdata.Ed25519VkLog as default
+ breq: testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog2), &testdata.Ed25519SkWitness, &testdata.Ed25519VkWitness),
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ description: "valid",
+ breq: testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness, &testdata.Ed25519VkWitness),
+ wantCode: http.StatusOK,
+ },
+ } {
+ func() { // run deferred functions at the end of each iteration
+ ti := newTestInstance(t, nil)
+ defer ti.ctrl.Finish()
+
+ url := EndpointAddCosignature.Path("http://example.com", ti.instance.LogParameters.Prefix)
+ req, err := http.NewRequest("POST", url, table.breq)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ w := httptest.NewRecorder()
+ ti.postHandler(t, EndpointAddCosignature).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+func TestEndpointGetLatestSth(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ trsp *trillian.GetLatestSignedLogRootResponse
+ terr error
+ wantCode int
+ wantItem *types.StItem
+ }{
+ {
+ description: "backend failure",
+ terr: fmt.Errorf("backend failure"),
+ wantCode: http.StatusInternalServerError,
+ },
+ {
+ description: "valid",
+ trsp: testdata.DefaultTSlr(t),
+ wantCode: http.StatusOK,
+ wantItem: testdata.DefaultSth(t, testdata.Ed25519VkLog),
+ },
+ } {
+ func() { // run deferred functions at the end of each iteration
+ ti := newTestInstance(t, cttestdata.NewSignerWithFixedSig(nil, testdata.Signature))
+ ti.ctrl.Finish()
+
+ // Setup and run client query
+ url := EndpointGetLatestSth.Path("http://example.com", ti.instance.LogParameters.Prefix)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+ if table.trsp != nil || table.terr != nil {
+ ti.client.EXPECT().GetLatestSignedLogRoot(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr)
+ }
+
+ w := httptest.NewRecorder()
+ ti.getHandler(t, EndpointGetLatestSth).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description)
+ }
+ if w.Code != http.StatusOK {
+ return
+ }
+
+ var item types.StItem
+ if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil {
+ t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err)
+ }
+ if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) {
+ t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+func TestEndpointGetStableSth(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ useBadSource bool
+ wantCode int
+ wantItem *types.StItem
+ }{
+ {
+ description: "invalid: sth source failure",
+ useBadSource: true,
+ wantCode: http.StatusInternalServerError,
+ },
+ {
+ description: "valid",
+ wantCode: http.StatusOK,
+ wantItem: testdata.DefaultSth(t, testdata.Ed25519VkLog),
+ },
+ } {
+ func() { // run deferred functions at the end of each iteration
+ ti := newTestInstance(t, nil)
+ ti.ctrl.Finish()
+ if table.useBadSource {
+ ti.instance.SthSource = &ActiveSthSource{}
+ }
+
+ // Setup and run client query
+ url := EndpointGetStableSth.Path("http://example.com", ti.instance.LogParameters.Prefix)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ w := httptest.NewRecorder()
+ ti.getHandler(t, EndpointGetStableSth).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description)
+ }
+ if w.Code != http.StatusOK {
+ return
+ }
+
+ var item types.StItem
+ if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil {
+ t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err)
+ }
+ if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) {
+ t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+func TestEndpointGetCosignedSth(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ useBadSource bool
+ wantCode int
+ wantItem *types.StItem
+ }{
+ {
+ description: "invalid: sth source failure",
+ useBadSource: true,
+ wantCode: http.StatusInternalServerError,
+ },
+ {
+ description: "valid",
+ wantCode: http.StatusOK,
+ wantItem: testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}),
+ },
+ } {
+ func() { // run deferred functions at the end of each iteration
+ ti := newTestInstance(t, nil)
+ ti.ctrl.Finish()
+ if table.useBadSource {
+ ti.instance.SthSource = &ActiveSthSource{}
+ }
+
+ // Setup and run client query
+ url := EndpointGetCosignedSth.Path("http://example.com", ti.instance.LogParameters.Prefix)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+
+ w := httptest.NewRecorder()
+ ti.getHandler(t, EndpointGetCosignedSth).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description)
+ }
+ if w.Code != http.StatusOK {
+ return
+ }
+
+ var item types.StItem
+ if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil {
+ t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err)
+ }
+ if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) {
+ t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+func TestEndpointGetProofByHash(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ breq *bytes.Buffer
+ trsp *trillian.GetInclusionProofByHashResponse
+ terr error
+ wantCode int
+ wantItem *types.StItem
+ }{
+ {
+ description: "invalid: bad request: empty",
+ breq: bytes.NewBuffer(nil),
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ description: "invalid: bad Trillian response: error",
+ breq: bytes.NewBuffer(marshal(t, types.GetProofByHashV1{TreeSize: 1, Hash: testdata.LeafHash})),
+ terr: fmt.Errorf("backend failure"),
+ wantCode: http.StatusInternalServerError,
+ },
+ {
+ description: "valid",
+ breq: bytes.NewBuffer(marshal(t, types.GetProofByHashV1{TreeSize: 1, Hash: testdata.LeafHash})),
+ trsp: testdata.DefaultTGipbhr(t),
+ wantCode: http.StatusOK,
+ wantItem: testdata.DefaultInclusionProof(t, 1),
+ },
+ } {
+ func() { // run deferred functions at the end of each iteration
+ ti := newTestInstance(t, nil)
+ defer ti.ctrl.Finish()
+
+ url := EndpointGetProofByHash.Path("http://example.com", ti.instance.LogParameters.Prefix)
+ req, err := http.NewRequest("POST", url, table.breq)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+ if table.trsp != nil || table.terr != nil {
+ ti.client.EXPECT().GetInclusionProofByHash(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr)
+ }
+
+ w := httptest.NewRecorder()
+ ti.postHandler(t, EndpointGetProofByHash).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description)
+ }
+ if w.Code != http.StatusOK {
+ return
+ }
+
+ var item types.StItem
+ if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil {
+ t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err)
+ }
+ if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) {
+ t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+func TestEndpointGetConsistencyProof(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ breq *bytes.Buffer
+ trsp *trillian.GetConsistencyProofResponse
+ terr error
+ wantCode int
+ wantItem *types.StItem
+ }{
+ {
+ description: "invalid: bad request: empty",
+ breq: bytes.NewBuffer(nil),
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ description: "invalid: bad Trillian response: error",
+ breq: bytes.NewBuffer(marshal(t, types.GetConsistencyProofV1{First: 1, Second: 2})),
+ terr: fmt.Errorf("backend failure"),
+ wantCode: http.StatusInternalServerError,
+ },
+ {
+ description: "valid",
+ breq: bytes.NewBuffer(marshal(t, types.GetConsistencyProofV1{First: 1, Second: 2})),
+ trsp: testdata.DefaultTGcpr(t),
+ wantCode: http.StatusOK,
+ wantItem: testdata.DefaultConsistencyProof(t, 1, 2),
+ },
+ } {
+ func() { // run deferred functions at the end of each iteration
+ ti := newTestInstance(t, nil)
+ defer ti.ctrl.Finish()
+
+ url := EndpointGetConsistencyProof.Path("http://example.com", ti.instance.LogParameters.Prefix)
+ req, err := http.NewRequest("POST", url, table.breq)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+ if table.trsp != nil || table.terr != nil {
+ ti.client.EXPECT().GetConsistencyProof(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr)
+ }
+
+ w := httptest.NewRecorder()
+ ti.postHandler(t, EndpointGetConsistencyProof).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description)
+ }
+ if w.Code != http.StatusOK {
+ return
+ }
+
+ var item types.StItem
+ if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil {
+ t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err)
+ }
+ if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) {
+ t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+func TestEndpointGetEntriesV1(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ breq *bytes.Buffer
+ trsp *trillian.GetLeavesByRangeResponse
+ terr error
+ wantCode int
+ wantItem *types.StItemList
+ }{
+ {
+ description: "invalid: bad request: empty",
+ breq: bytes.NewBuffer(nil),
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ description: "invalid: bad Trillian response: error",
+ breq: bytes.NewBuffer(marshal(t, types.GetEntriesV1{Start: 0, End: 0})),
+ terr: fmt.Errorf("backend failure"),
+ wantCode: http.StatusInternalServerError,
+ },
+ {
+ description: "valid", // remember that newLogParameters() have testdata.MaxRange configured
+ breq: bytes.NewBuffer(marshal(t, types.GetEntriesV1{Start: 0, End: uint64(testdata.MaxRange - 1)})),
+ trsp: testdata.DefaultTGlbrr(t, 0, testdata.MaxRange-1),
+ wantCode: http.StatusOK,
+ wantItem: testdata.DefaultStItemList(t, 0, uint64(testdata.MaxRange)-1),
+ },
+ } {
+ func() { // run deferred functions at the end of each iteration
+ ti := newTestInstance(t, nil)
+ defer ti.ctrl.Finish()
+
+ url := EndpointGetEntries.Path("http://example.com", ti.instance.LogParameters.Prefix)
+ req, err := http.NewRequest("POST", url, table.breq)
+ if err != nil {
+ t.Fatalf("must create http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+ if table.trsp != nil || table.terr != nil {
+ ti.client.EXPECT().GetLeavesByRange(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr)
+ }
+
+ w := httptest.NewRecorder()
+ ti.postHandler(t, EndpointGetEntries).ServeHTTP(w, req)
+ if got, want := w.Code, table.wantCode; got != want {
+ t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description)
+ }
+ if w.Code != http.StatusOK {
+ return
+ }
+
+ var item types.StItemList
+ if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil {
+ t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err)
+ }
+ if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) {
+ t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description)
+ }
+ }()
+ }
+}
+
+// TODO: TestWriteOctetResponse
+func TestWriteOctetResponse(t *testing.T) {
+}
+
+// deadlineMatcher implements gomock.Matcher, such that an error is raised if
+// there is no context.Context deadline set
+type deadlineMatcher struct{}
+
+// newDeadlineMatcher returns a new DeadlineMatcher
+func newDeadlineMatcher() gomock.Matcher {
+ return &deadlineMatcher{}
+}
+
+// Matches returns true if the passed interface is a context with a deadline
+func (dm *deadlineMatcher) Matches(i interface{}) bool {
+ ctx, ok := i.(context.Context)
+ if !ok {
+ return false
+ }
+ _, ok = ctx.Deadline()
+ return ok
+}
+
+// String is needed to implement gomock.Matcher
+func (dm *deadlineMatcher) String() string {
+ return fmt.Sprintf("deadlineMatcher{}")
+}
diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go
new file mode 100644
index 0000000..3441a0a
--- /dev/null
+++ b/pkg/instance/instance.go
@@ -0,0 +1,90 @@
+package stfe
+
+import (
+ "context"
+ "crypto"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/golang/glog"
+ "github.com/system-transparency/stfe/pkg/state"
+ "github.com/system-transparency/stfe/pkg/trillian"
+ "github.com/system-transparency/stfe/pkg/types"
+)
+
+// Config is a collection of log parameters
+type Config struct {
+ LogID string // H(public key), then hex-encoded
+ TreeID int64 // Merkle tree identifier used by Trillian
+ Prefix string // The portion between base URL and st/v0 (may be "")
+ MaxRange int64 // Maximum number of leaves per get-leaves request
+ Deadline time.Duration // Deadline used for gRPC requests
+ Interval time.Duration // Cosigning frequency
+
+ // Witnesses map trusted witness identifiers to public verification keys
+ Witnesses map[[types.HashSize]byte][types.VerificationKeySize]byte
+}
+
+// Instance is an instance of the log's front-end
+type Instance struct {
+ Config // configuration parameters
+ Client trillian.Client // provides access to the Trillian backend
+ Signer crypto.Signer // provides access to Ed25519 private key
+ Stateman state.StateManager // coordinates access to (co)signed tree heads
+}
+
+// Handler implements the http.Handler interface, and contains a reference
+// to an STFE server instance as well as a function that uses it.
+type Handler struct {
+ Instance *Instance
+ Endpoint types.Endpoint
+ Method string
+ Handler func(context.Context, *Instance, http.ResponseWriter, *http.Request) (int, error)
+}
+
+// Handlers returns a list of STFE handlers
+func (i *Instance) Handlers() []Handler {
+ return []Handler{
+ Handler{Instance: i, Handler: addLeaf, Endpoint: types.EndpointAddLeaf, Method: http.MethodPost},
+ Handler{Instance: i, Handler: addCosignature, Endpoint: types.EndpointAddCosignature, Method: http.MethodPost},
+ Handler{Instance: i, Handler: getTreeHeadLatest, Endpoint: types.EndpointGetTreeHeadLatest, Method: http.MethodGet},
+ Handler{Instance: i, Handler: getTreeHeadToSign, Endpoint: types.EndpointGetTreeHeadToSign, Method: http.MethodGet},
+ Handler{Instance: i, Handler: getTreeHeadCosigned, Endpoint: types.EndpointGetTreeHeadCosigned, Method: http.MethodGet},
+ Handler{Instance: i, Handler: getConsistencyProof, Endpoint: types.EndpointGetConsistencyProof, Method: http.MethodPost},
+ Handler{Instance: i, Handler: getInclusionProof, Endpoint: types.EndpointGetProofByHash, Method: http.MethodPost},
+ Handler{Instance: i, Handler: getLeaves, Endpoint: types.EndpointGetLeaves, Method: http.MethodPost},
+ }
+}
+
+// Path returns a path that should be configured for this handler
+func (h Handler) Path() string {
+ return h.Endpoint.Path(h.Instance.Prefix, "st", "v0")
+}
+
+// ServeHTTP is part of the http.Handler interface
+func (a Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // export prometheus metrics
+ var now time.Time = time.Now()
+ var statusCode int
+ defer func() {
+ rspcnt.Inc(a.Instance.LogID, string(a.Endpoint), fmt.Sprintf("%d", statusCode))
+ latency.Observe(time.Now().Sub(now).Seconds(), a.Instance.LogID, string(a.Endpoint), fmt.Sprintf("%d", statusCode))
+ }()
+ reqcnt.Inc(a.Instance.LogID, string(a.Endpoint))
+
+ ctx, cancel := context.WithDeadline(r.Context(), now.Add(a.Instance.Deadline))
+ defer cancel()
+
+ if r.Method != a.Method {
+ glog.Warningf("%s/%s: got HTTP %s, wanted HTTP %s", a.Instance.Prefix, string(a.Endpoint), r.Method, a.Method)
+ http.Error(w, "", http.StatusMethodNotAllowed)
+ return
+ }
+
+ statusCode, err := a.Handler(ctx, a.Instance, w, r)
+ if err != nil {
+ glog.Warningf("handler error %s/%s: %v", a.Instance.Prefix, a.Endpoint, err)
+ http.Error(w, fmt.Sprintf("%s%s%s%s", "Error", types.Delim, err.Error(), types.EOL), statusCode)
+ }
+}
diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go
new file mode 100644
index 0000000..a7a3d8a
--- /dev/null
+++ b/pkg/instance/instance_test.go
@@ -0,0 +1,158 @@
+package stfe
+
+import (
+ "crypto"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/google/certificate-transparency-go/trillian/mockclient"
+ "github.com/system-transparency/stfe/pkg/testdata"
+ "github.com/system-transparency/stfe/pkg/types"
+)
+
+type testInstance struct {
+ ctrl *gomock.Controller
+ client *mockclient.MockTrillianLogClient
+ instance *Instance
+}
+
+// newTestInstances sets up a test instance that uses default log parameters
+// with an optional signer, see newLogParameters() for further details. The
+// SthSource is instantiated with an ActiveSthSource that has (i) the default
+// STH as the currently cosigned STH based on testdata.Ed25519VkWitness, and
+// (ii) the default STH without any cosignatures as the currently stable STH.
+func newTestInstance(t *testing.T, signer crypto.Signer) *testInstance {
+ t.Helper()
+ ctrl := gomock.NewController(t)
+ client := mockclient.NewMockTrillianLogClient(ctrl)
+ return &testInstance{
+ ctrl: ctrl,
+ client: client,
+ instance: &Instance{
+ Client: client,
+ LogParameters: newLogParameters(t, signer),
+ SthSource: &ActiveSthSource{
+ client: client,
+ logParameters: newLogParameters(t, signer),
+ currCosth: testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}),
+ nextCosth: testdata.DefaultCosth(t, testdata.Ed25519VkLog, nil),
+ cosignatureFrom: make(map[[types.NamespaceFingerprintSize]byte]bool),
+ },
+ },
+ }
+}
+
+// getHandlers returns all endpoints that use HTTP GET as a map to handlers
+func (ti *testInstance) getHandlers(t *testing.T) map[Endpoint]Handler {
+ t.Helper()
+ return map[Endpoint]Handler{
+ EndpointGetLatestSth: Handler{Instance: ti.instance, Handler: getLatestSth, Endpoint: EndpointGetLatestSth, Method: http.MethodGet},
+ EndpointGetStableSth: Handler{Instance: ti.instance, Handler: getStableSth, Endpoint: EndpointGetStableSth, Method: http.MethodGet},
+ EndpointGetCosignedSth: Handler{Instance: ti.instance, Handler: getCosignedSth, Endpoint: EndpointGetCosignedSth, Method: http.MethodGet},
+ }
+}
+
+// postHandlers returns all endpoints that use HTTP POST as a map to handlers
+func (ti *testInstance) postHandlers(t *testing.T) map[Endpoint]Handler {
+ t.Helper()
+ return map[Endpoint]Handler{
+ EndpointAddEntry: Handler{Instance: ti.instance, Handler: addEntry, Endpoint: EndpointAddEntry, Method: http.MethodPost},
+ EndpointAddCosignature: Handler{Instance: ti.instance, Handler: addCosignature, Endpoint: EndpointAddCosignature, Method: http.MethodPost},
+ EndpointGetConsistencyProof: Handler{Instance: ti.instance, Handler: getConsistencyProof, Endpoint: EndpointGetConsistencyProof, Method: http.MethodPost},
+ EndpointGetProofByHash: Handler{Instance: ti.instance, Handler: getProofByHash, Endpoint: EndpointGetProofByHash, Method: http.MethodPost},
+ EndpointGetEntries: Handler{Instance: ti.instance, Handler: getEntries, Endpoint: EndpointGetEntries, Method: http.MethodPost},
+ }
+}
+
+// getHandler must return a particular HTTP GET handler
+func (ti *testInstance) getHandler(t *testing.T, endpoint Endpoint) Handler {
+ t.Helper()
+ handler, ok := ti.getHandlers(t)[endpoint]
+ if !ok {
+ t.Fatalf("must return HTTP GET handler for endpoint: %s", endpoint)
+ }
+ return handler
+}
+
+// postHandler must return a particular HTTP POST handler
+func (ti *testInstance) postHandler(t *testing.T, endpoint Endpoint) Handler {
+ t.Helper()
+ handler, ok := ti.postHandlers(t)[endpoint]
+ if !ok {
+ t.Fatalf("must return HTTP POST handler for endpoint: %s", endpoint)
+ }
+ return handler
+}
+
+// TestHandlers checks that we configured all endpoints and that there are no
+// unexpected ones.
+func TestHandlers(t *testing.T) {
+ endpoints := map[Endpoint]bool{
+ EndpointAddEntry: false,
+ EndpointAddCosignature: false,
+ EndpointGetLatestSth: false,
+ EndpointGetStableSth: false,
+ EndpointGetCosignedSth: false,
+ EndpointGetConsistencyProof: false,
+ EndpointGetProofByHash: false,
+ EndpointGetEntries: false,
+ }
+ i := &Instance{nil, newLogParameters(t, nil), nil}
+ for _, handler := range i.Handlers() {
+ if _, ok := endpoints[handler.Endpoint]; !ok {
+ t.Errorf("got unexpected endpoint: %s", handler.Endpoint)
+ }
+ endpoints[handler.Endpoint] = true
+ }
+ for endpoint, ok := range endpoints {
+ if !ok {
+ t.Errorf("endpoint %s is not configured", endpoint)
+ }
+ }
+}
+
+// TestGetHandlersRejectPost checks that all get handlers reject post requests
+func TestGetHandlersRejectPost(t *testing.T) {
+ ti := newTestInstance(t, nil)
+ defer ti.ctrl.Finish()
+
+ for endpoint, handler := range ti.getHandlers(t) {
+ t.Run(string(endpoint), func(t *testing.T) {
+ s := httptest.NewServer(handler)
+ defer s.Close()
+
+ url := endpoint.Path(s.URL, ti.instance.LogParameters.Prefix)
+ if rsp, err := http.Post(url, "application/json", nil); err != nil {
+ t.Fatalf("http.Post(%s)=(_,%q), want (_,nil)", url, err)
+ } else if rsp.StatusCode != http.StatusMethodNotAllowed {
+ t.Errorf("http.Post(%s)=(%d,nil), want (%d, nil)", url, rsp.StatusCode, http.StatusMethodNotAllowed)
+ }
+ })
+ }
+}
+
+// TestPostHandlersRejectGet checks that all post handlers reject get requests
+func TestPostHandlersRejectGet(t *testing.T) {
+ ti := newTestInstance(t, nil)
+ defer ti.ctrl.Finish()
+
+ for endpoint, handler := range ti.postHandlers(t) {
+ t.Run(string(endpoint), func(t *testing.T) {
+ s := httptest.NewServer(handler)
+ defer s.Close()
+
+ url := endpoint.Path(s.URL, ti.instance.LogParameters.Prefix)
+ if rsp, err := http.Get(url); err != nil {
+ t.Fatalf("http.Get(%s)=(_,%q), want (_,nil)", url, err)
+ } else if rsp.StatusCode != http.StatusMethodNotAllowed {
+ t.Errorf("http.Get(%s)=(%d,nil), want (%d, nil)", url, rsp.StatusCode, http.StatusMethodNotAllowed)
+ }
+ })
+ }
+}
+
+// TODO: TestHandlerPath
+func TestHandlerPath(t *testing.T) {
+}
diff --git a/pkg/instance/metric.go b/pkg/instance/metric.go
new file mode 100644
index 0000000..7e3e8b2
--- /dev/null
+++ b/pkg/instance/metric.go
@@ -0,0 +1,23 @@
+package stfe
+
+import (
+ "github.com/google/trillian/monitoring"
+ "github.com/google/trillian/monitoring/prometheus"
+)
+
+var (
+ reqcnt monitoring.Counter // number of incoming http requests
+ rspcnt monitoring.Counter // number of valid http responses
+ latency monitoring.Histogram // request-response latency
+ lastSthTimestamp monitoring.Gauge // unix timestamp from the most recent sth
+ lastSthSize monitoring.Gauge // tree size of most recent sth
+)
+
+func init() {
+ mf := prometheus.MetricFactory{}
+ reqcnt = mf.NewCounter("http_req", "number of http requests", "logid", "endpoint")
+ rspcnt = mf.NewCounter("http_rsp", "number of http requests", "logid", "endpoint", "status")
+ latency = mf.NewHistogram("http_latency", "http request-response latency", "logid", "endpoint", "status")
+ lastSthTimestamp = mf.NewGauge("last_sth_timestamp", "unix timestamp while handling the most recent sth", "logid")
+ lastSthSize = mf.NewGauge("last_sth_size", "most recent sth tree size", "logid")
+}
diff --git a/pkg/instance/request.go b/pkg/instance/request.go
new file mode 100644
index 0000000..7475b26
--- /dev/null
+++ b/pkg/instance/request.go
@@ -0,0 +1,77 @@
+package stfe
+
+import (
+ "crypto/ed25519"
+ "fmt"
+ "net/http"
+
+ "github.com/system-transparency/stfe/pkg/types"
+)
+
+func (i *Instance) leafRequestFromHTTP(r *http.Request) (*types.LeafRequest, error) {
+ var req types.LeafRequest
+ if err := req.UnmarshalASCII(r.Body); err != nil {
+ return nil, fmt.Errorf("UnmarshalASCII: %v", err)
+ }
+
+ vk := ed25519.PublicKey(req.VerificationKey[:])
+ msg := req.Message.Marshal()
+ sig := req.Signature[:]
+ if !ed25519.Verify(vk, msg, sig) {
+ return nil, fmt.Errorf("invalid signature")
+ }
+ // TODO: check shard hint
+ // TODO: check domain hint
+ return &req, nil
+}
+
+func (i *Instance) cosignatureRequestFromHTTP(r *http.Request) (*types.CosignatureRequest, error) {
+ var req types.CosignatureRequest
+ if err := req.UnmarshalASCII(r.Body); err != nil {
+ return nil, fmt.Errorf("unpackOctetPost: %v", err)
+ }
+ if _, ok := i.Witnesses[*req.KeyHash]; !ok {
+ return nil, fmt.Errorf("Unknown witness: %x", req.KeyHash)
+ }
+ return &req, nil
+}
+
+func (i *Instance) consistencyProofRequestFromHTTP(r *http.Request) (*types.ConsistencyProofRequest, error) {
+ var req types.ConsistencyProofRequest
+ if err := req.UnmarshalASCII(r.Body); err != nil {
+ return nil, fmt.Errorf("UnmarshalASCII: %v", err)
+ }
+ if req.OldSize < 1 {
+ return nil, fmt.Errorf("OldSize(%d) must be larger than zero", req.OldSize)
+ }
+ if req.NewSize <= req.OldSize {
+ return nil, fmt.Errorf("NewSize(%d) must be larger than OldSize(%d)", req.NewSize, req.OldSize)
+ }
+ return &req, nil
+}
+
+func (i *Instance) inclusionProofRequestFromHTTP(r *http.Request) (*types.InclusionProofRequest, error) {
+ var req types.InclusionProofRequest
+ if err := req.UnmarshalASCII(r.Body); err != nil {
+ return nil, fmt.Errorf("UnmarshalASCII: %v", err)
+ }
+ if req.TreeSize < 1 {
+ return nil, fmt.Errorf("TreeSize(%d) must be larger than zero", req.TreeSize)
+ }
+ return &req, nil
+}
+
+func (i *Instance) leavesRequestFromHTTP(r *http.Request) (*types.LeavesRequest, error) {
+ var req types.LeavesRequest
+ if err := req.UnmarshalASCII(r.Body); err != nil {
+ return nil, fmt.Errorf("UnmarshalASCII: %v", err)
+ }
+
+ if req.StartSize > req.EndSize {
+ return nil, fmt.Errorf("StartSize(%d) must be less than or equal to EndSize(%d)", req.StartSize, req.EndSize)
+ }
+ if req.EndSize-req.StartSize+1 > uint64(i.MaxRange) {
+ req.EndSize = req.StartSize + uint64(i.MaxRange) - 1
+ }
+ return &req, nil
+}
diff --git a/pkg/instance/request_test.go b/pkg/instance/request_test.go
new file mode 100644
index 0000000..0a5a908
--- /dev/null
+++ b/pkg/instance/request_test.go
@@ -0,0 +1,318 @@
+package stfe
+
+import (
+ "bytes"
+ //"fmt"
+ "reflect"
+ "testing"
+ //"testing/iotest"
+
+ "net/http"
+
+ "github.com/system-transparency/stfe/pkg/testdata"
+ "github.com/system-transparency/stfe/pkg/types"
+)
+
+func TestParseAddEntryV1Request(t *testing.T) {
+ lp := newLogParameters(t, nil)
+ for _, table := range []struct {
+ description string
+ breq *bytes.Buffer
+ wantErr bool
+ }{
+ {
+ description: "invalid: nothing to unpack",
+ breq: bytes.NewBuffer(nil),
+ wantErr: true,
+ },
+ {
+ description: "invalid: not a signed checksum entry",
+ breq: testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness, &testdata.Ed25519VkWitness),
+ wantErr: true,
+ },
+ {
+ description: "invalid: untrusted submitter", // only testdata.Ed25519VkSubmitter is registered by default in newLogParameters()
+
+ breq: testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter2, testdata.Ed25519VkSubmitter2),
+ wantErr: true,
+ },
+ {
+ description: "invalid: signature does not cover message",
+
+ breq: testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter2, testdata.Ed25519VkSubmitter),
+ wantErr: true,
+ },
+ {
+ description: "valid",
+ breq: testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter, testdata.Ed25519VkSubmitter),
+ }, // TODO: add test case that disables submitter policy (i.e., unregistered namespaces are accepted)
+ } {
+ url := EndpointAddEntry.Path("http://example.com", lp.Prefix)
+ req, err := http.NewRequest("POST", url, table.breq)
+ if err != nil {
+ t.Fatalf("failed creating http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ _, err = lp.parseAddEntryV1Request(req)
+ if got, want := err != nil, table.wantErr; got != want {
+ t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err)
+ }
+ }
+}
+
+func TestParseAddCosignatureV1Request(t *testing.T) {
+ lp := newLogParameters(t, nil)
+ for _, table := range []struct {
+ description string
+ breq *bytes.Buffer
+ wantErr bool
+ }{
+ {
+ description: "invalid: nothing to unpack",
+ breq: bytes.NewBuffer(nil),
+ wantErr: true,
+ },
+ {
+ description: "invalid: not a cosigned sth",
+ breq: testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter, testdata.Ed25519VkSubmitter),
+ wantErr: true,
+ },
+ {
+ description: "invalid: no cosignature",
+ breq: testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness, nil),
+ wantErr: true,
+ },
+ {
+ description: "invalid: untrusted witness", // only testdata.Ed25519VkWitness is registered by default in newLogParameters()
+ breq: testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness2, &testdata.Ed25519VkWitness2),
+ wantErr: true,
+ },
+ {
+ description: "invalid: signature does not cover message",
+ breq: testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness2, &testdata.Ed25519VkWitness),
+ wantErr: true,
+ },
+ {
+ description: "valid",
+ breq: testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness, &testdata.Ed25519VkWitness),
+ }, // TODO: add test case that disables witness policy (i.e., unregistered namespaces are accepted)
+ } {
+ url := EndpointAddCosignature.Path("http://example.com", lp.Prefix)
+ req, err := http.NewRequest("POST", url, table.breq)
+ if err != nil {
+ t.Fatalf("failed creating http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ _, err = lp.parseAddCosignatureV1Request(req)
+ if got, want := err != nil, table.wantErr; got != want {
+ t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err)
+ }
+ }
+}
+
+func TestNewGetConsistencyProofRequest(t *testing.T) {
+ lp := newLogParameters(t, nil)
+ for _, table := range []struct {
+ description string
+ req *types.GetConsistencyProofV1
+ wantErr bool
+ }{
+ {
+ description: "invalid: nothing to unpack",
+ req: nil,
+ wantErr: true,
+ },
+ {
+ description: "invalid: first must be larger than zero",
+ req: &types.GetConsistencyProofV1{First: 0, Second: 0},
+ wantErr: true,
+ },
+ {
+ description: "invalid: second must be larger than first",
+ req: &types.GetConsistencyProofV1{First: 2, Second: 1},
+ wantErr: true,
+ },
+ {
+ description: "valid",
+ req: &types.GetConsistencyProofV1{First: 1, Second: 2},
+ },
+ } {
+ var buf *bytes.Buffer
+ if table.req == nil {
+ buf = bytes.NewBuffer(nil)
+ } else {
+ buf = bytes.NewBuffer(marshal(t, *table.req))
+ }
+
+ url := EndpointGetConsistencyProof.Path("http://example.com", lp.Prefix)
+ req, err := http.NewRequest("POST", url, buf)
+ if err != nil {
+ t.Fatalf("failed creating http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ _, err = lp.parseGetConsistencyProofV1Request(req)
+ if got, want := err != nil, table.wantErr; got != want {
+ t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err)
+ }
+ }
+}
+
+func TestNewGetProofByHashRequest(t *testing.T) {
+ lp := newLogParameters(t, nil)
+ for _, table := range []struct {
+ description string
+ req *types.GetProofByHashV1
+ wantErr bool
+ }{
+ {
+ description: "invalid: nothing to unpack",
+ req: nil,
+ wantErr: true,
+ },
+ {
+ description: "invalid: no entry in an empty tree",
+ req: &types.GetProofByHashV1{TreeSize: 0, Hash: testdata.LeafHash},
+ wantErr: true,
+ },
+ {
+ description: "valid",
+ req: &types.GetProofByHashV1{TreeSize: 1, Hash: testdata.LeafHash},
+ },
+ } {
+ var buf *bytes.Buffer
+ if table.req == nil {
+ buf = bytes.NewBuffer(nil)
+ } else {
+ buf = bytes.NewBuffer(marshal(t, *table.req))
+ }
+
+ url := EndpointGetProofByHash.Path("http://example.com", lp.Prefix)
+ req, err := http.NewRequest("POST", url, buf)
+ if err != nil {
+ t.Fatalf("failed creating http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ _, err = lp.parseGetProofByHashV1Request(req)
+ if got, want := err != nil, table.wantErr; got != want {
+ t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err)
+ }
+ }
+}
+
+func TestParseGetEntriesV1Request(t *testing.T) {
+ lp := newLogParameters(t, nil)
+ for _, table := range []struct {
+ description string
+ req *types.GetEntriesV1
+ wantErr bool
+ wantReq *types.GetEntriesV1
+ }{
+ {
+ description: "invalid: nothing to unpack",
+ req: nil,
+ wantErr: true,
+ },
+ {
+ description: "invalid: start must be larger than end",
+ req: &types.GetEntriesV1{Start: 1, End: 0},
+ wantErr: true,
+ },
+ {
+ description: "valid: want truncated range",
+ req: &types.GetEntriesV1{Start: 0, End: uint64(testdata.MaxRange)},
+ wantReq: &types.GetEntriesV1{Start: 0, End: uint64(testdata.MaxRange) - 1},
+ },
+ {
+ description: "valid",
+ req: &types.GetEntriesV1{Start: 0, End: 0},
+ wantReq: &types.GetEntriesV1{Start: 0, End: 0},
+ },
+ } {
+ var buf *bytes.Buffer
+ if table.req == nil {
+ buf = bytes.NewBuffer(nil)
+ } else {
+ buf = bytes.NewBuffer(marshal(t, *table.req))
+ }
+
+ url := EndpointGetEntries.Path("http://example.com", lp.Prefix)
+ req, err := http.NewRequest("POST", url, buf)
+ if err != nil {
+ t.Fatalf("failed creating http request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ output, err := lp.parseGetEntriesV1Request(req)
+ if got, want := err != nil, table.wantErr; got != want {
+ t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err)
+ }
+ if err != nil {
+ continue
+ }
+ if got, want := output, table.wantReq; !reflect.DeepEqual(got, want) {
+ t.Errorf("got request\n%v\n\tbut wanted\n%v\n\t in test %q", got, want, table.description)
+ }
+ }
+}
+
+func TestUnpackOctetPost(t *testing.T) {
+ for _, table := range []struct {
+ description string
+ req *http.Request
+ out interface{}
+ wantErr bool
+ }{
+ //{
+ // description: "invalid: cannot read request body",
+ // req: func() *http.Request {
+ // req, err := http.NewRequest(http.MethodPost, "", iotest.ErrReader(fmt.Errorf("bad reader")))
+ // if err != nil {
+ // t.Fatalf("must make new http request: %v", err)
+ // }
+ // return req
+ // }(),
+ // out: &types.StItem{},
+ // wantErr: true,
+ //}, // testcase requires Go 1.16
+ {
+ description: "invalid: cannot unmarshal",
+ req: func() *http.Request {
+ req, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer(nil))
+ if err != nil {
+ t.Fatalf("must make new http request: %v", err)
+ }
+ return req
+ }(),
+ out: &types.StItem{},
+ wantErr: true,
+ },
+ {
+ description: "valid",
+ req: func() *http.Request {
+ req, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer([]byte{0}))
+ if err != nil {
+ t.Fatalf("must make new http request: %v", err)
+ }
+ return req
+ }(),
+ out: &struct{ SomeUint8 uint8 }{},
+ },
+ } {
+ err := unpackOctetPost(table.req, table.out)
+ if got, want := err != nil, table.wantErr; got != want {
+ t.Errorf("got error %v but wanted %v in test %q", got, want, table.description)
+ }
+ }
+}
+
+func marshal(t *testing.T, out interface{}) []byte {
+ b, err := types.Marshal(out)
+ if err != nil {
+ t.Fatalf("must marshal: %v", err)
+ }
+ return b
+}