From 0ffc7bb2f016a7c9c23f3ff07f6b8e5b09363338 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 15 Jan 2021 16:51:36 +0100 Subject: [PATCH] Moved Nuts-Network to Nuts-Node and implemented RFC004 --- .codeclimate.yml | 14 +- .gitignore | 2 + README.rst | 71 +- cmd/root.go | 26 +- cmd/root_test.go | 21 + codecov.yml | 13 +- core/config_test.go | 2 - core/engine.go | 6 + core/mock.go | 501 +++++++++++ crypto/crypto.go | 5 +- crypto/engine/engine.go | 4 +- crypto/engine/engine_test.go | 13 +- crypto/hash/test.go | 23 + crypto/interface.go | 19 +- crypto/jwx.go | 27 +- crypto/jwx_test.go | 35 + crypto/mock.go | 15 + .../images/network_component_diagram.svg | 3 + ...network_configuration_artifact_diagram.svg | 3 + docs/_static/images/network_engine_layers.svg | 3 + .../images/network_layouts_directwan.svg | 3 + .../images/network_layouts_tlsoffloading.svg | 3 + .../images/network_layouts_tlspassthrough.svg | 3 + docs/_static/network/v1.yaml | 72 ++ docs/diagrams/network architecture.drawio | 1 + .../network infrastructure layouts.drawio | 1 + docs/pages/configuration/options.rst | 15 +- docs/pages/development.rst | 18 +- generate_readme.sh | 2 +- go.mod | 6 +- go.sum | 312 +------ makefile | 11 +- network/api/v1/api.go | 89 ++ network/api/v1/api_test.go | 183 ++++ network/api/v1/client.go | 133 +++ network/api/v1/client_test.go | 107 +++ network/api/v1/generated.go | 528 +++++++++++ network/config.go | 60 ++ network/config_test.go | 59 ++ network/dag/bboltdag.go | 480 ++++++++++ network/dag/bboltdag_test.go | 120 +++ network/dag/dag.go | 162 ++++ network/dag/dag_contract_test.go | 332 +++++++ network/dag/dag_test.go | 122 +++ network/dag/document.go | 200 +++++ network/dag/document_test.go | 120 +++ network/dag/dotviz_test.go | 90 ++ network/dag/mock.go | 320 +++++++ network/dag/parser.go | 212 +++++ network/dag/parser_test.go | 306 +++++++ network/dag/signing.go | 114 +++ network/dag/signing_test.go | 86 ++ network/dag/test.go | 62 ++ network/engine/engine.go | 156 ++++ network/engine/engine_test.go | 111 +++ network/interface.go | 42 + network/log/logger.go | 30 + network/mock.go | 113 +++ network/network.go | 194 +++++ network/network_integration_test.go | 212 +++++ network/network_test.go | 284 ++++++ network/p2p/backoff.go | 62 ++ network/p2p/backoff_test.go | 51 ++ network/p2p/impl-peer.go | 105 +++ network/p2p/impl-util.go | 62 ++ network/p2p/impl-util_test.go | 75 ++ network/p2p/impl.go | 406 +++++++++ network/p2p/impl_test.go | 128 +++ network/p2p/interface.go | 78 ++ network/p2p/mock.go | 210 +++++ network/p2p/stats.go | 57 ++ network/proto/handlers.go | 187 ++++ network/proto/impl.go | 175 ++++ network/proto/interface.go | 72 ++ network/proto/mock.go | 124 +++ network/proto/stats.go | 80 ++ network/proto/stats_test.go | 33 + network/test.go | 45 + network/test/certificate-and-key.pem | 46 + network/test/generate.sh | 21 + network/test/localhost.ext | 8 + network/test/truststore.pem | 17 + network/transport/network.pb.go | 818 ++++++++++++++++++ network/transport/network.proto | 73 ++ test/io/io.go | 18 + test/io/io_test.go | 18 + vdr/ambassador.go | 56 ++ vdr/doc-creator_test.go | 6 +- vdr/engine/engine.go | 17 +- vdr/engine/engine_test.go | 19 +- vdr/network/ambassador.go | 83 -- vdr/network/mock.go | 45 - vdr/vdr.go | 28 +- 93 files changed, 8981 insertions(+), 522 deletions(-) create mode 100644 core/mock.go create mode 100644 crypto/hash/test.go create mode 100644 docs/_static/images/network_component_diagram.svg create mode 100644 docs/_static/images/network_configuration_artifact_diagram.svg create mode 100644 docs/_static/images/network_engine_layers.svg create mode 100644 docs/_static/images/network_layouts_directwan.svg create mode 100644 docs/_static/images/network_layouts_tlsoffloading.svg create mode 100644 docs/_static/images/network_layouts_tlspassthrough.svg create mode 100644 docs/_static/network/v1.yaml create mode 100644 docs/diagrams/network architecture.drawio create mode 100644 docs/diagrams/network infrastructure layouts.drawio create mode 100644 network/api/v1/api.go create mode 100644 network/api/v1/api_test.go create mode 100644 network/api/v1/client.go create mode 100644 network/api/v1/client_test.go create mode 100644 network/api/v1/generated.go create mode 100644 network/config.go create mode 100644 network/config_test.go create mode 100644 network/dag/bboltdag.go create mode 100644 network/dag/bboltdag_test.go create mode 100644 network/dag/dag.go create mode 100644 network/dag/dag_contract_test.go create mode 100644 network/dag/dag_test.go create mode 100644 network/dag/document.go create mode 100644 network/dag/document_test.go create mode 100644 network/dag/dotviz_test.go create mode 100644 network/dag/mock.go create mode 100644 network/dag/parser.go create mode 100644 network/dag/parser_test.go create mode 100644 network/dag/signing.go create mode 100644 network/dag/signing_test.go create mode 100644 network/dag/test.go create mode 100644 network/engine/engine.go create mode 100644 network/engine/engine_test.go create mode 100644 network/interface.go create mode 100644 network/log/logger.go create mode 100644 network/mock.go create mode 100644 network/network.go create mode 100644 network/network_integration_test.go create mode 100644 network/network_test.go create mode 100644 network/p2p/backoff.go create mode 100644 network/p2p/backoff_test.go create mode 100644 network/p2p/impl-peer.go create mode 100644 network/p2p/impl-util.go create mode 100644 network/p2p/impl-util_test.go create mode 100644 network/p2p/impl.go create mode 100644 network/p2p/impl_test.go create mode 100644 network/p2p/interface.go create mode 100644 network/p2p/mock.go create mode 100644 network/p2p/stats.go create mode 100644 network/proto/handlers.go create mode 100644 network/proto/impl.go create mode 100644 network/proto/interface.go create mode 100644 network/proto/mock.go create mode 100644 network/proto/stats.go create mode 100644 network/proto/stats_test.go create mode 100644 network/test.go create mode 100644 network/test/certificate-and-key.pem create mode 100755 network/test/generate.sh create mode 100644 network/test/localhost.ext create mode 100644 network/test/truststore.pem create mode 100644 network/transport/network.pb.go create mode 100644 network/transport/network.proto create mode 100644 vdr/ambassador.go delete mode 100644 vdr/network/ambassador.go delete mode 100644 vdr/network/mock.go diff --git a/.codeclimate.yml b/.codeclimate.yml index 1d4ffdd5d1..716fe1fefe 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -7,13 +7,17 @@ checks: return-statements: enabled: false exclude_patterns: - - "docs/" + - "**/generated.go" + - "**/test/**/*.go" + - "**/test.go" - "**/*_test.go" - - '**/test.go' - - "test/*" + - "**/mock/**/*.go" - "**/mock.go" - - "**/generated.go" - - "**/mock/*" + - "docs/**/*.go" + - "**/*.pb.go" + # Until network protocol implementation has been spec and optionally replace, ignore those sources + - "network/p2p/**/*.go" + - "network/proto/**/*.go" plugins: gofmt: diff --git a/.gitignore b/.gitignore index 3ba1681b55..8f8a6ca28e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ data # ignore generated pem files from running the nuts-node executable. /*.pem + +/nuts-node \ No newline at end of file diff --git a/README.rst b/README.rst index 35d0dcf025..022e77ab0b 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Distributed registry for storing and querying health care providers their vendor Dependencies ************ -This projects is using go modules, so version > 1.12 is recommended. 1.10 would be a minimum. +Go >= 1.15 is required. Running tests ************* @@ -38,20 +38,22 @@ Building Just use ``go build``. -The server and client API is generated from the open-api spec: +Code Generation +*************** -.. code-block:: shell +Code generation is used for generating mocks, OpenAPI client- and servers and gRPC services. To regenerate the code +run the `run-generators` target from the Makefile: - make gen-api +.. code-block:: shell -Generating Mocks -**************** + make run-generators -These mocks are used by other modules +The peer-to-peer API uses gRPC. To generate Go code from the protobuf specs you need the `protoc-gen-go` package: .. code-block:: shell - make gen-mocks + go get -u github.com/golang/protobuf/protoc-gen-go + README ****** @@ -122,5 +124,56 @@ The following options can be configured: .. marker-for-config-options -.. include:: options.rst +======================================== =================================================================================== ================================================================================================================================================================================ +Key Default Description +======================================== =================================================================================== ================================================================================================================================================================================ +**** +address localhost:1323 Address and port the server will be listening to +configfile nuts.yaml Nuts config file +identity Vendor identity for the node, mandatory when running in server mode. Must be in the format: urn:oid:1.3.6.1.4.1.54851.4: +mode server Mode the application will run in. When 'cli' it can be used to administer a remote Nuts node. When 'server' it will start a Nuts node. Defaults to 'server'. +strictmode false When set, insecure settings are forbidden. +verbosity info Log level (trace, debug, info, warn, error) +**Auth** +auth.actingPartyCn The acting party Common name used in contracts +auth.address localhost:1323 Interface and port for http server to bind to +auth.enableCORS false Set if you want to allow CORS requests. This is useful when you want browsers to directly communicate with the nuts node. +auth.irmaConfigPath path to IRMA config folder. If not set, a tmp folder is created. +auth.irmaSchemeManager pbdf The IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo' +auth.mode server or client, when client it does not start any services so that CLI commands can be used. +auth.publicUrl Public URL which can be reached by a users IRMA client +auth.skipAutoUpdateIrmaSchemas false set if you want to skip the auto download of the irma schemas every 60 minutes. +**ConsentBridgeClient** +cbridge.address http://localhost:8080 API Address of the consent bridge +**ConsentStore** +cstore.address localhost:1323 Address of the server when in client mode +cstore.connectionstring \:memory: Db connectionString +cstore.mode server or client, when client it uses the HttpClient +**Crypto** +crypto.fspath ./ when file system is used as storage, this configures the path where keys are stored (default .) +crypto.keysize 2048 number of bits to use when creating new RSA keys +crypto.storage fs storage to use, 'fs' for file system (default) +**Events octopus** +events.autoRecover false Republish unfinished events at startup +events.connectionstring file::memory:?cache=shared db connection string for event store +events.incrementalBackoff 8 Incremental backoff per retry queue, queue 0 retries after 1 second, queue 1 after {incrementalBackoff} * {previousDelay} +events.maxRetryCount 5 Max number of retries for events before giving up (only for recoverable errors +events.natsPort 4222 Port for Nats to bind on +events.purgeCompleted false Purge completed events at startup +events.retryInterval 60 Retry delay in seconds for reconnecting +**Network** +network.bootstrapNodes Space-separated list of bootstrap nodes (`:`) which the node initially connect to. +network.certFile PEM file containing the certificate this node will identify itself with to other nodes. If not set, the Nuts node will attempt to load a TLS certificate from the crypto module. +network.certKeyFile PEM file containing the key belonging to this node's certificate. If not set, the Nuts node will attempt to load a TLS certificate from the crypto module. +network.grpcAddr \:5555 Local address for gRPC to listen on. +network.publicAddr Public address (of this node) other nodes can use to connect to it. If set, it is registered on the nodelist. +network.databaseFile network.db Path to BBolt database file for storage of the network. +network.trustStoreFile PEM file containing the trusted CA certificates for authenticating remote gRPC servers. +network.advertHashesInterval 2000 Interval (in milliseconds) that specifies how often the node should broadcast its last hashes to other nodes. +**VDR** +vdr.clientTimeout 10 Time-out for the client in seconds (e.g. when using the CLI), default: 10 +vdr.datadir ./data Location of data files, default: ./data +**Validation** +fhir.schemapath location of json schema, default nested Asset +=========================================================================================================================== ================================================================================================================================================================================ diff --git a/cmd/root.go b/cmd/root.go index ba2fa34b27..4fdb302221 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,11 +24,20 @@ import ( "github.com/labstack/echo/v4/middleware" "github.com/nuts-foundation/nuts-node/core" crypto "github.com/nuts-foundation/nuts-node/crypto/engine" + "github.com/nuts-foundation/nuts-node/network/engine" vdr "github.com/nuts-foundation/nuts-node/vdr/engine" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) +// Allows overriding Echo server implementation to aid testing +var echoCreator = func() core.EchoServer { + echo := echo.New() + echo.HideBanner = true + echo.Use(middleware.Logger()) + return echo +} + func createRootCommand() *cobra.Command { return &cobra.Command{ Use: "nuts", @@ -41,10 +50,7 @@ func createRootCommand() *cobra.Command { return } // start interfaces - echo := echo.New() - echo.HideBanner = true - echo.Use(middleware.Logger()) - + echo := echoCreator() for _, engine := range core.EngineCtl.Engines { if engine.Routes != nil { engine.Routes(echo) @@ -52,7 +58,9 @@ func createRootCommand() *cobra.Command { } defer shutdownEngines() - logrus.Fatal(echo.Start(cfg.ServerAddress())) + if err := echo.Start(cfg.ServerAddress()); err != nil { + logrus.Fatal(err) + } }, } } @@ -104,9 +112,11 @@ func registerEngines() { core.RegisterEngine(core.NewStatusEngine()) core.RegisterEngine(core.NewLoggerEngine()) core.RegisterEngine(core.NewMetricsEngine()) - core.RegisterEngine(crypto.NewCryptoEngine()) - core.RegisterEngine(vdr.NewVDREngine()) - + cryptoEngine, keyStore := crypto.NewCryptoEngine() + core.RegisterEngine(cryptoEngine) + networkEngine, networkInstance := engine.NewNetworkEngine(keyStore) + core.RegisterEngine(networkEngine) + core.RegisterEngine(vdr.NewVDREngine(keyStore, networkInstance)) } func injectConfig(cfg *core.NutsGlobalConfig) { diff --git a/cmd/root_test.go b/cmd/root_test.go index 7a439f9d0c..f9776d6c7b 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,13 +1,33 @@ package cmd import ( + "github.com/golang/mock/gomock" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/test/io" "github.com/stretchr/testify/assert" "os" + "path" "testing" ) func Test_rootCmd(t *testing.T) { + t.Run("start in server mode", func(t *testing.T) { + ctrl := gomock.NewController(t) + echoServer := core.NewMockEchoServer(ctrl) + echoServer.EXPECT().GET(gomock.Any(), gomock.Any()).AnyTimes() + echoServer.EXPECT().POST(gomock.Any(), gomock.Any()).AnyTimes() + echoServer.EXPECT().PUT(gomock.Any(), gomock.Any()).AnyTimes() + echoServer.EXPECT().Start(gomock.Any()) + echoCreator = func() core.EchoServer { + return echoServer + } + testDirectory := io.TestDirectory(t) + os.Setenv("NUTS_NETWORK_DATABASEFILE", path.Join(testDirectory, "network.db")) + os.Setenv("NUTS_VDR_DATADIR", path.Join(testDirectory, "vdr")) + os.Setenv("NUTS_CRYPTO_FSPATH", path.Join(testDirectory, "crypto")) + os.Args = []string{"nuts"} + Execute() + }) t.Run("start in CLI mode", func(t *testing.T) { var routesCalled = false core.RegisterEngine(&core.Engine{ @@ -20,4 +40,5 @@ func Test_rootCmd(t *testing.T) { assert.NoError(t, CreateCommand().Execute()) assert.False(t, routesCalled, "engine.Routes was called") }) + } diff --git a/codecov.yml b/codecov.yml index 569e94c062..815bb278d9 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,14 @@ ignore: - "**/generated.go" - - "test/*" - - '**/test.go' - - "mock/*" + - "**/test/" + - "**/test.go" + - "**/*_test.go" + - "mock/**/*.go" - "**/mock.go" - - "docs/*" + - "docs/**/*.go" + - "**/*.pb.go" + # Until network protocol implementation has been spec and optionally replace, ignore those sources + - "network/p2p/" + - "network/proto/" github_checks: annotations: false diff --git a/core/config_test.go b/core/config_test.go index c9d73790de..2bd68481d9 100644 --- a/core/config_test.go +++ b/core/config_test.go @@ -304,8 +304,6 @@ func TestNutsGlobalConfig_LoadConfigFile(t *testing.T) { func TestNutsGlobalConfig_LoadAndUnmarshal(t *testing.T) { cfg := NewNutsGlobalConfig() - os.Setenv("NUTS_IDENTITY", "urn:oid:1.3.6.1.4.1.54851.4:4") - defer os.Unsetenv("NUTS_IDENTITY") cfg.Load(&cobra.Command{}) type mandatory struct { diff --git a/core/engine.go b/core/engine.go index 041a3cb2e5..a7b3bcaf1f 100644 --- a/core/engine.go +++ b/core/engine.go @@ -35,6 +35,12 @@ type EngineControl struct { var EngineCtl EngineControl +// EchoServer implements both the EchoRouter interface and Start function to aid testing. +type EchoServer interface { + EchoRouter + Start(address string) error +} + // EchoRouter is the interface the generated server API's will require as the Routes func argument type EchoRouter interface { CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route diff --git a/core/mock.go b/core/mock.go new file mode 100644 index 0000000000..5cd61fbadb --- /dev/null +++ b/core/mock.go @@ -0,0 +1,501 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: core/engine.go + +// Package core is a generated GoMock package. +package core + +import ( + gomock "github.com/golang/mock/gomock" + echo "github.com/labstack/echo/v4" + reflect "reflect" +) + +// MockEchoServer is a mock of EchoServer interface +type MockEchoServer struct { + ctrl *gomock.Controller + recorder *MockEchoServerMockRecorder +} + +// MockEchoServerMockRecorder is the mock recorder for MockEchoServer +type MockEchoServerMockRecorder struct { + mock *MockEchoServer +} + +// NewMockEchoServer creates a new mock instance +func NewMockEchoServer(ctrl *gomock.Controller) *MockEchoServer { + mock := &MockEchoServer{ctrl: ctrl} + mock.recorder = &MockEchoServerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockEchoServer) EXPECT() *MockEchoServerMockRecorder { + return m.recorder +} + +// CONNECT mocks base method +func (m_2 *MockEchoServer) CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "CONNECT", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// CONNECT indicates an expected call of CONNECT +func (mr *MockEchoServerMockRecorder) CONNECT(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CONNECT", reflect.TypeOf((*MockEchoServer)(nil).CONNECT), varargs...) +} + +// DELETE mocks base method +func (m_2 *MockEchoServer) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "DELETE", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// DELETE indicates an expected call of DELETE +func (mr *MockEchoServerMockRecorder) DELETE(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DELETE", reflect.TypeOf((*MockEchoServer)(nil).DELETE), varargs...) +} + +// GET mocks base method +func (m_2 *MockEchoServer) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "GET", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// GET indicates an expected call of GET +func (mr *MockEchoServerMockRecorder) GET(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GET", reflect.TypeOf((*MockEchoServer)(nil).GET), varargs...) +} + +// HEAD mocks base method +func (m_2 *MockEchoServer) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "HEAD", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// HEAD indicates an expected call of HEAD +func (mr *MockEchoServerMockRecorder) HEAD(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HEAD", reflect.TypeOf((*MockEchoServer)(nil).HEAD), varargs...) +} + +// OPTIONS mocks base method +func (m_2 *MockEchoServer) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "OPTIONS", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// OPTIONS indicates an expected call of OPTIONS +func (mr *MockEchoServerMockRecorder) OPTIONS(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OPTIONS", reflect.TypeOf((*MockEchoServer)(nil).OPTIONS), varargs...) +} + +// PATCH mocks base method +func (m_2 *MockEchoServer) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "PATCH", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// PATCH indicates an expected call of PATCH +func (mr *MockEchoServerMockRecorder) PATCH(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PATCH", reflect.TypeOf((*MockEchoServer)(nil).PATCH), varargs...) +} + +// POST mocks base method +func (m_2 *MockEchoServer) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "POST", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// POST indicates an expected call of POST +func (mr *MockEchoServerMockRecorder) POST(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "POST", reflect.TypeOf((*MockEchoServer)(nil).POST), varargs...) +} + +// PUT mocks base method +func (m_2 *MockEchoServer) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "PUT", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// PUT indicates an expected call of PUT +func (mr *MockEchoServerMockRecorder) PUT(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PUT", reflect.TypeOf((*MockEchoServer)(nil).PUT), varargs...) +} + +// TRACE mocks base method +func (m_2 *MockEchoServer) TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "TRACE", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// TRACE indicates an expected call of TRACE +func (mr *MockEchoServerMockRecorder) TRACE(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TRACE", reflect.TypeOf((*MockEchoServer)(nil).TRACE), varargs...) +} + +// Start mocks base method +func (m *MockEchoServer) Start(address string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start", address) + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *MockEchoServerMockRecorder) Start(address interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockEchoServer)(nil).Start), address) +} + +// MockEchoRouter is a mock of EchoRouter interface +type MockEchoRouter struct { + ctrl *gomock.Controller + recorder *MockEchoRouterMockRecorder +} + +// MockEchoRouterMockRecorder is the mock recorder for MockEchoRouter +type MockEchoRouterMockRecorder struct { + mock *MockEchoRouter +} + +// NewMockEchoRouter creates a new mock instance +func NewMockEchoRouter(ctrl *gomock.Controller) *MockEchoRouter { + mock := &MockEchoRouter{ctrl: ctrl} + mock.recorder = &MockEchoRouterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockEchoRouter) EXPECT() *MockEchoRouterMockRecorder { + return m.recorder +} + +// CONNECT mocks base method +func (m_2 *MockEchoRouter) CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "CONNECT", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// CONNECT indicates an expected call of CONNECT +func (mr *MockEchoRouterMockRecorder) CONNECT(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CONNECT", reflect.TypeOf((*MockEchoRouter)(nil).CONNECT), varargs...) +} + +// DELETE mocks base method +func (m_2 *MockEchoRouter) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "DELETE", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// DELETE indicates an expected call of DELETE +func (mr *MockEchoRouterMockRecorder) DELETE(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DELETE", reflect.TypeOf((*MockEchoRouter)(nil).DELETE), varargs...) +} + +// GET mocks base method +func (m_2 *MockEchoRouter) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "GET", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// GET indicates an expected call of GET +func (mr *MockEchoRouterMockRecorder) GET(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GET", reflect.TypeOf((*MockEchoRouter)(nil).GET), varargs...) +} + +// HEAD mocks base method +func (m_2 *MockEchoRouter) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "HEAD", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// HEAD indicates an expected call of HEAD +func (mr *MockEchoRouterMockRecorder) HEAD(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HEAD", reflect.TypeOf((*MockEchoRouter)(nil).HEAD), varargs...) +} + +// OPTIONS mocks base method +func (m_2 *MockEchoRouter) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "OPTIONS", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// OPTIONS indicates an expected call of OPTIONS +func (mr *MockEchoRouterMockRecorder) OPTIONS(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OPTIONS", reflect.TypeOf((*MockEchoRouter)(nil).OPTIONS), varargs...) +} + +// PATCH mocks base method +func (m_2 *MockEchoRouter) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "PATCH", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// PATCH indicates an expected call of PATCH +func (mr *MockEchoRouterMockRecorder) PATCH(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PATCH", reflect.TypeOf((*MockEchoRouter)(nil).PATCH), varargs...) +} + +// POST mocks base method +func (m_2 *MockEchoRouter) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "POST", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// POST indicates an expected call of POST +func (mr *MockEchoRouterMockRecorder) POST(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "POST", reflect.TypeOf((*MockEchoRouter)(nil).POST), varargs...) +} + +// PUT mocks base method +func (m_2 *MockEchoRouter) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "PUT", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// PUT indicates an expected call of PUT +func (mr *MockEchoRouterMockRecorder) PUT(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PUT", reflect.TypeOf((*MockEchoRouter)(nil).PUT), varargs...) +} + +// TRACE mocks base method +func (m_2 *MockEchoRouter) TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route { + m_2.ctrl.T.Helper() + varargs := []interface{}{path, h} + for _, a := range m { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "TRACE", varargs...) + ret0, _ := ret[0].(*echo.Route) + return ret0 +} + +// TRACE indicates an expected call of TRACE +func (mr *MockEchoRouterMockRecorder) TRACE(path, h interface{}, m ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{path, h}, m...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TRACE", reflect.TypeOf((*MockEchoRouter)(nil).TRACE), varargs...) +} + +// MockRunnable is a mock of Runnable interface +type MockRunnable struct { + ctrl *gomock.Controller + recorder *MockRunnableMockRecorder +} + +// MockRunnableMockRecorder is the mock recorder for MockRunnable +type MockRunnableMockRecorder struct { + mock *MockRunnable +} + +// NewMockRunnable creates a new mock instance +func NewMockRunnable(ctrl *gomock.Controller) *MockRunnable { + mock := &MockRunnable{ctrl: ctrl} + mock.recorder = &MockRunnableMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockRunnable) EXPECT() *MockRunnableMockRecorder { + return m.recorder +} + +// Start mocks base method +func (m *MockRunnable) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *MockRunnableMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockRunnable)(nil).Start)) +} + +// Shutdown mocks base method +func (m *MockRunnable) Shutdown() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Shutdown") + ret0, _ := ret[0].(error) + return ret0 +} + +// Shutdown indicates an expected call of Shutdown +func (mr *MockRunnableMockRecorder) Shutdown() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockRunnable)(nil).Shutdown)) +} + +// MockConfigurable is a mock of Configurable interface +type MockConfigurable struct { + ctrl *gomock.Controller + recorder *MockConfigurableMockRecorder +} + +// MockConfigurableMockRecorder is the mock recorder for MockConfigurable +type MockConfigurableMockRecorder struct { + mock *MockConfigurable +} + +// NewMockConfigurable creates a new mock instance +func NewMockConfigurable(ctrl *gomock.Controller) *MockConfigurable { + mock := &MockConfigurable{ctrl: ctrl} + mock.recorder = &MockConfigurableMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockConfigurable) EXPECT() *MockConfigurableMockRecorder { + return m.recorder +} + +// Configure mocks base method +func (m *MockConfigurable) Configure() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Configure") + ret0, _ := ret[0].(error) + return ret0 +} + +// Configure indicates an expected call of Configure +func (mr *MockConfigurableMockRecorder) Configure() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configure", reflect.TypeOf((*MockConfigurable)(nil).Configure)) +} diff --git a/crypto/crypto.go b/crypto/crypto.go index 87b7ae6f78..3af0410950 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -24,12 +24,11 @@ import ( "crypto/elliptic" "crypto/rand" "errors" - "io" - "sync" - "github.com/lestrrat-go/jwx/jwk" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto/storage" + "io" + "sync" ) // Config holds the values for the crypto engine diff --git a/crypto/engine/engine.go b/crypto/engine/engine.go index 25823282e0..83bcd357df 100644 --- a/crypto/engine/engine.go +++ b/crypto/engine/engine.go @@ -40,7 +40,7 @@ const ConfigStorage string = "storage" const ConfigFSPath string = "fspath" // NewCryptoEngine the engine configuration for nuts-go. -func NewCryptoEngine() *core.Engine { +func NewCryptoEngine() (*core.Engine, crypto2.KeyStore) { cb := crypto2.Instance() return &core.Engine{ @@ -54,7 +54,7 @@ func NewCryptoEngine() *core.Engine { api.RegisterHandlers(router, &api.Wrapper{C: cb}) }, Runnable: cb, - } + }, cb } // FlagSet returns the configuration flags for crypto diff --git a/crypto/engine/engine_test.go b/crypto/engine/engine_test.go index 16e374bde9..bdde01746b 100644 --- a/crypto/engine/engine_test.go +++ b/crypto/engine/engine_test.go @@ -38,13 +38,13 @@ import ( func TestNewCryptoEngine(t *testing.T) { t.Run("New returns an engine with Cmd and Routes", func(t *testing.T) { - client := NewCryptoEngine() + engine, _ := NewCryptoEngine() - if client.Cmd == nil { + if engine.Cmd == nil { t.Errorf("Expected Engine to have Cmd") } - if client.Routes == nil { + if engine.Routes == nil { t.Errorf("Expected Engine to have Routes") } }) @@ -52,7 +52,7 @@ func TestNewCryptoEngine(t *testing.T) { func TestNewCryptoEngine_Routes(t *testing.T) { t.Run("Registers the available routes", func(t *testing.T) { - ce := NewCryptoEngine() + ce, _ := NewCryptoEngine() ctrl := gomock.NewController(t) defer ctrl.Finish() echo := mock.NewMockEchoRouter(ctrl) @@ -88,7 +88,8 @@ func TestNewCryptoEngine_Cmd(t *testing.T) { createCmd := func(t *testing.T) (*cobra.Command, *crypto.Crypto) { testDirectory := io.TestDirectory(t) instance := crypto.NewTestCryptoInstance(testDirectory) - return NewCryptoEngine().Cmd, instance + engine, _ := NewCryptoEngine() + return engine.Cmd, instance } t.Run("publicKey", func(t *testing.T) { @@ -137,7 +138,7 @@ func TestNewCryptoEngine_Cmd(t *testing.T) { func TestNewCryptoEngine_FlagSet(t *testing.T) { t.Run("Cobra help should list flags", func(t *testing.T) { - e := NewCryptoEngine() + e, _ := NewCryptoEngine() cmd := newRootCommand() cmd.Flags().AddFlagSet(e.FlagSet) cmd.SetArgs([]string{"--help"}) diff --git a/crypto/hash/test.go b/crypto/hash/test.go new file mode 100644 index 0000000000..4eeac9b034 --- /dev/null +++ b/crypto/hash/test.go @@ -0,0 +1,23 @@ +package hash + +import "github.com/golang/mock/gomock" + +func EqHash(hash SHA256Hash) gomock.Matcher { + return &hashMatcher{expected: hash} +} + +type hashMatcher struct { + expected SHA256Hash +} + +func (h hashMatcher) Matches(x interface{}) bool { + if actual, ok := x.(SHA256Hash); !ok { + return false + } else { + return actual.Equals(h.expected) + } +} + +func (h hashMatcher) String() string { + return "Hashes matches: " + h.expected.String() +} diff --git a/crypto/interface.go b/crypto/interface.go index 852a19a77f..5c3480625e 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -32,19 +32,30 @@ type KeyCreator interface { New(namingFunc KIDNamingFunc) (crypto.PublicKey, string, error) } +// KeyResolver defines the functions for retrieving keys. +type KeyResolver interface { + // GetPublicKey returns the PublicKey + // If a key is missing, a Storage.ErrNotFound is returned + GetPublicKey(kid string) (crypto.PublicKey, error) +} + // KeyStore defines the functions that can be called by a Cmd, Direct or via rest call. type KeyStore interface { KeyCreator - + KeyResolver + JWSSigner // GetPrivateKey returns the specified private key (for e.g. signing) in non-exportable form. // If a key is missing, a Storage.ErrNotFound is returned GetPrivateKey(kid string) (crypto.Signer, error) - // GetPublicKey returns the PublicKey - // If a key is missing, a Storage.ErrNotFound is returned - GetPublicKey(kid string) (crypto.PublicKey, error) // SignJWT creates a signed JWT using the given key and map of claims (private key must be present). SignJWT(claims map[string]interface{}, kid string) (string, error) // PrivateKeyExists returns if the specified private key exists. // If an error occurs, false is also returned PrivateKeyExists(kid string) bool } + +// JWSSigner defines the functions for signing JSON Web Signatures. +type JWSSigner interface { + // SignJWS creates a signed JWS (in compact form using) the given key (private key must be present), protected headers and payload. + SignJWS(payload []byte, protectedHeaders map[string]interface{}, kid string) (string, error) +} diff --git a/crypto/jwx.go b/crypto/jwx.go index 00025b75fb..f714dcd09d 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -23,7 +23,7 @@ import ( "crypto/ecdsa" "crypto/rsa" "errors" - + "fmt" "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jws" @@ -53,6 +53,31 @@ func (client *Crypto) SignJWT(claims map[string]interface{}, kid string) (token return } +// SignJWS creates a signed JWS (in compact form using) the given key (private key must be present), protected headers and payload. +func (client *Crypto) SignJWS(payload []byte, protectedHeaders map[string]interface{}, kid string) (string, error) { + headers := jws.NewHeaders() + for key, value := range protectedHeaders { + if err := headers.Set(key, value); err != nil { + return "", fmt.Errorf("unable to set header %s: %w", key, err) + } + } + privateKey, err := client.Storage.GetPrivateKey(kid) + if err != nil { + return "", fmt.Errorf("error while signing JWS, can't get private key: %w", err) + } + privateKeyAsJWK, err := jwkKey(privateKey) + if err != nil { + return "", err + } + algo := jwa.SignatureAlgorithm(privateKeyAsJWK.Algorithm()) + protectedHeaders[jws.AlgorithmKey] = algo + data, err := jws.Sign(payload, algo, privateKey, jws.WithHeaders(headers)) + if err != nil { + return "", fmt.Errorf("unable to sign JWS %w", err) + } + return string(data), nil +} + func jwkKey(signer crypto.Signer) (key jwk.Key, err error) { key, err = jwk.New(signer) if err != nil { diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index 9d6124d7bd..5ebdb6950f 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -24,6 +24,7 @@ import ( "crypto/elliptic" "crypto/rand" "fmt" + "strings" "testing" "github.com/lestrrat-go/jwx/jwa" @@ -133,6 +134,40 @@ func TestCrypto_SignJWT(t *testing.T) { }) } +func TestCrypto_SignJWS(t *testing.T) { + client := createCrypto(t) + kid := "kid" + client.New(StringNamingFunc(kid)) + + t.Run("ok", func(t *testing.T) { + payload := []byte{1, 2, 3} + signature, err := client.SignJWS(payload, map[string]interface{}{"foo": "bar"}, kid) + if !assert.NoError(t, err) { + return + } + message, err := jws.Parse(strings.NewReader(signature)) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, payload, message.Payload()) + assert.Len(t, message.Signatures(), 1) + value, _ := message.Signatures()[0].ProtectedHeaders().Get("foo") + assert.Equal(t, "bar", value.(string)) + }) + t.Run("invalid header", func(t *testing.T) { + payload := []byte{1, 2, 3} + signature, err := client.SignJWS(payload, map[string]interface{}{"jwk": "invalid jwk"}, kid) + assert.EqualError(t, err, "unable to set header jwk: invalid value for jwk key: string") + assert.Empty(t, signature) + }) + t.Run("unknown key", func(t *testing.T) { + payload := []byte{1, 2, 3} + signature, err := client.SignJWS(payload, map[string]interface{}{}, "unknown") + assert.Contains(t, err.Error(), "error while signing JWS, can't get private key") + assert.Empty(t, signature) + }) +} + func TestCrypto_convertHeaders(t *testing.T) { t.Run("nil headers", func(t *testing.T) { jwtHeader := convertHeaders(nil) diff --git a/crypto/mock.go b/crypto/mock.go index 6b29695a18..6be2fef1a1 100644 --- a/crypto/mock.go +++ b/crypto/mock.go @@ -133,6 +133,21 @@ func (mr *MockKeyStoreMockRecorder) SignJWT(claims, kid interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignJWT", reflect.TypeOf((*MockKeyStore)(nil).SignJWT), claims, kid) } +// SignJWS mocks base method +func (m *MockKeyStore) SignJWS(payload []byte, protectedHeaders map[string]interface{}, kid string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SignJWS", payload, protectedHeaders, kid) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SignJWS indicates an expected call of SignJWS +func (mr *MockKeyStoreMockRecorder) SignJWS(payload, protectedHeaders, kid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignJWS", reflect.TypeOf((*MockKeyStore)(nil).SignJWS), payload, protectedHeaders, kid) +} + // PrivateKeyExists mocks base method func (m *MockKeyStore) PrivateKeyExists(kid string) bool { m.ctrl.T.Helper() diff --git a/docs/_static/images/network_component_diagram.svg b/docs/_static/images/network_component_diagram.svg new file mode 100644 index 0000000000..fdf7ea79a1 --- /dev/null +++ b/docs/_static/images/network_component_diagram.svg @@ -0,0 +1,3 @@ + + +
NodeList
[Component]
Discovers new peers by reading nodelist from the network
NodeList...
Subscribe
Subscribe
Registry / Consent
[Engine]
Uses network to build an application
Registry / Consent...
read
write
read...
Send notifications
Send notifications
DocumentLog
[Component]
Builds hash list and associated documents
DocumentLog...
Send messages
Send messages
Exchange hashes & documents
[gRPC]
Exchange hashes & documents...
P2PNetwork
[Component]
Builds and maintains connections with remote peers
P2PNetwork...
Add remote peers
Add remote peers
Add/Get document
Add/List document hash
Add/Get do...
Resolve remote hashes
Advert local hash
Resolve re...
Register node info
(add document)
Register n...
Read documents
[queue]
Read docum...
Protocol
[Component]
Abstracts gRPC messages; queries the network, responds to peer queries
Protocol...
Read received hashes
[queue]
Read recei...
Internal
Internal
Legend
Legend
External
External
Network Engine
Network Engine
Add documents
[REST]
Add docume...
Document store
[Database]
Document store...
Read messages
[queue]
Read messages...
Call direction
Call direction
API
[Component]
Exposes operations to remote systems
API...
DocumentNotifications
[Component, NATS]
Exposes document notifications to interested systems
DocumentNotifications...
Network
[Component]
Orchestrates the engine, collects diagnostics
Network...
Collect diagnostics
[REST]
Collect diagnostics...
Monitor
[External System]
Monitors state of the engine
Monitor...
Remote Nuts Node
[External System]
Remote Nuts Node...
Administer
[REST]
Administer...
Command-Line Interface
[Application]
Remote administration of the engine
Command-Line Interface...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/images/network_configuration_artifact_diagram.svg b/docs/_static/images/network_configuration_artifact_diagram.svg new file mode 100644 index 0000000000..cb89e73ac8 --- /dev/null +++ b/docs/_static/images/network_configuration_artifact_diagram.svg @@ -0,0 +1,3 @@ + + +
nuts-registry
[Engine]
nuts-registry...
nuts-network
[Engine]
nuts-network...
Nuts Certificate Authority
Trust Bundle

[Configuration]
Nuts Certificate...
Trust bundle for
TLS, signatures
Trust bund...
Network Bootstrap Nodes
[Configuration]
Network Bootstra...
Initial peers
Initial pe...
Node Certificate
+ private key

[Configuration]
Node Certificate...
Auth with TLS certs
Auth with...
Produces
(issues)
Produces...
Publishes
(issues)
Publishes...
Nuts Foundation
[Organization]
Nuts Foundation...
Publishes
Publishes
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/images/network_engine_layers.svg b/docs/_static/images/network_engine_layers.svg new file mode 100644 index 0000000000..65b58bf51f --- /dev/null +++ b/docs/_static/images/network_engine_layers.svg @@ -0,0 +1,3 @@ + + +
DocumentLog
Builds a consistent document log and persists it
DocumentLog...
P2P
Builds and maintains gRPC connections with remote peers
P2P...
Protocol
 Abstracts gRPC messages; queries the network, responds to peer queries
Protocol...
Application
Systems that build their application on top of the distributed document log
Application...
NodeList
Discovers new peers by reading nodelist from the network
NodeList...
Registry
Registers vendors, care orgs and endpoints
Registry...
Consent
Registers patient consents across vendors/orgs
Consent...
Applications
Applications
Network Engine Layers
Network Engine Layers
Transport
TLS / TCP
Transport...
Interfaces
Exposes functionality to applications
Interfaces...
REST API
Diagnostics, querying, adding & retrieving
REST API...
Message Queue
Document notifications
Message Queue...
Interfaces
Interfaces
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/images/network_layouts_directwan.svg b/docs/_static/images/network_layouts_directwan.svg new file mode 100644 index 0000000000..e002d6c997 --- /dev/null +++ b/docs/_static/images/network_layouts_directwan.svg @@ -0,0 +1,3 @@ + + +
gRPC / HTTP2 over TLS %3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Behind%20reverse%20proxy%26lt%3Bbr%26gt%3BSSL%20terminator%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dleft%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D20%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22250%22%20y%3D%2240%22%20width%3D%22350%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E 
gRPC / HTTP2 over TLS %3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Behind%20reverse%20proxy%26lt%3Bbr%26gt%3BSSL%20terminator%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dleft%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D20%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22250%22%20y%3D%2240%22%20width%3D%22350%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E 
Nuts Node
Nuts Node
Public
Internet
Public...


Server Certificate
[ Private Key ]
Server Certificat...
Uses
Uses
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/images/network_layouts_tlsoffloading.svg b/docs/_static/images/network_layouts_tlsoffloading.svg new file mode 100644 index 0000000000..631c04561a --- /dev/null +++ b/docs/_static/images/network_layouts_tlsoffloading.svg @@ -0,0 +1,3 @@ + + +
gRPC / HTTP2 over TLS
gRPC / HTTP2 over TLS
Produces
Produces
Nuts Node
Nuts Node
Public
Internet
Public...
Plain gRPC / HTTP2
Plain gRPC / HTTP2
SSL/TLS offloader
[ HAProxy / Nginx ]
SSL/TLS offloader...
Uses
Uses


Server Certificate
[ Private Key ]
Server Certificate...
Authenticates client certs. using
Authenticates client certs. using


Truststore
[ X.509 certificate bundle ]
Truststore...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/images/network_layouts_tlspassthrough.svg b/docs/_static/images/network_layouts_tlspassthrough.svg new file mode 100644 index 0000000000..61ca1898f0 --- /dev/null +++ b/docs/_static/images/network_layouts_tlspassthrough.svg @@ -0,0 +1,3 @@ + + +
gRPC / HTTP2 over TLS%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Behind%20reverse%20proxy%26lt%3Bbr%26gt%3BSSL%20terminator%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dleft%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D20%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22250%22%20y%3D%2240%22%20width%3D%22350%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
gRPC / HTTP2 over TLS%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Behind%20reverse%20proxy%26lt%3Bbr%26gt%3BSSL%20terminator%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dleft%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D20%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22250%22%20y%3D%2240%22%20width%3D%22350%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
Nuts Node
Nuts Node
Public
Internet
Public...
gRPC / HTTP2 over TLS
gRPC / HTTP2 over TLS
Load Balancer
Load Balancer
Uses
Uses


Server Certificate
[ Private Key ]
Server Certificat...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/network/v1.yaml b/docs/_static/network/v1.yaml new file mode 100644 index 0000000000..8e42314c43 --- /dev/null +++ b/docs/_static/network/v1.yaml @@ -0,0 +1,72 @@ +openapi: "3.0.0" +info: + title: Nuts network API spec + description: API specification for RPC services available at the nuts-network + version: 0.1.0 + license: + name: GPLv3 +paths: + /api/document: + get: + summary: "Lists the documents on the DAG" + description: > + Lists all documents on the DAG. Since this call returns all documents on the DAG, care should be taken when there + are many of them. + TODO: By then we'd need a more elaborate querying interface (ranging over timestamps/hashes, pagination, filtering, etc). + operationId: "listDocuments" + tags: + - documents + responses: + "200": + description: "Successfully listed the documents" + content: + application/json: + schema: + type: array + items: + type: string + /api/document/{ref}: + parameters: + - name: ref + in: path + description: "Reference of the document" + required: true + example: "4960afbdf21280ef248081e6e52317735bbb929a204351291b773c252afeebf4" + schema: + type: string + get: + summary: "Retrieves a document" + operationId: "getDocument" + tags: + - documents + responses: + "200": + description: "Document is known in the document log" + content: + application/jose: + schema: + type: string + "404": + description: "Document wasn't found in the document log" + /api/document/{ref}/payload: + parameters: + - name: ref + in: path + description: "Reference of the document" + required: true + example: "4960afbdf21280ef248081e6e52317735bbb929a204351291b773c252afeebf4" + schema: + type: string + get: + summary: "Gets the document payload" + operationId: "getDocumentPayload" + tags: + - documents + responses: + "200": + description: "Document found (with payload) and returned." + content: + application/octet-stream: + example: + "404": + description: "Document (or payload) wasn't found" \ No newline at end of file diff --git a/docs/diagrams/network architecture.drawio b/docs/diagrams/network architecture.drawio new file mode 100644 index 0000000000..263ea94e47 --- /dev/null +++ b/docs/diagrams/network architecture.drawio @@ -0,0 +1 @@ +7VlNc9owEP01zKSHdPwFCccASTpt0mFC25yFrdgqstcjywHn13eFZWMj0iYMCemMT0hPK1m772m1Nj13HK+uBUmjWwgo7zlWsOq5k57j2G7fwh+FFCVy7tklEAoWaKMNMGNPVIN6XpizgGYtQwnAJUvboA9JQn3ZwogQsGybPQBvPzUlITWAmU+4id6zQEbai8othX+hLIyqJ9uWHolJZayBLCIBLBuQe9lzxwJAlq14NaZcBa+Kiw+TYrG4/3qXX7m/0+nq28/byWm52NVrptQuCJrIwy7tlEs/Ep7reH2ncgligeANKajQrsuiiqeAPAmoWtPquaNlxCSdpcRXo0tUEGKRjDn2bGxmUsCijjtGbBQKEjB0YwwcBGIJJDh19MA434I4mVM+hYxJBgnCPs7C/bijRyokQ4JvtgwkqKcTzsKd5hd6YA5SQqw2h9tmSfhDTVs7owHsnTqu2hQkcqY9t6t+KXC7j/0X0qLpU/ugq4YoNU3XFGIqRYEmetTTgqtOnO4uN/L1BhqLGtKtjxzRRyasV97IAhtaGa9QiWuoZCZBqIO3rY7nuIlZECgbg416oKmaiKRqvYiuSKimj1IqGLqi+KzQaQUpVRHha2IGan7ZtD47Zzs1+KyyDO28FcODNsP2DorrPNSkePBWDHsGwxOGgWPzfB2jjua9aLa3TrKDaeO4PPcNnseQZDTJ8qwjeT+SneGwfZi9wZFJHhgkTwV7JH7RUbwnxc5Hy9dnBsUXuYxAsCfSJezDJezjn+XzZxK2xHxtnYxBBOTTf1Sj17L6Z5leW/6lTLeOWKTXZXnRLtobSqnTRlMp3lspZWgo5Y6Gqogz036nj3d/idslD+895VG9HDZf9SE5FTTNA9ZdGgcsAPHWOPKlYdsG1/g65wP61NWAh6oBbffYLJsf71QRiH4jW+WBHnDcxGgusBWq1gkL1LAszJqhk8GLZNAffjQVmB/nflGRYXzUHdixvBfL7vn7HXbsbv5DWI81/olxL/8A7Zrdb5swEMD/GqT2YVP4SEoe87VWWidNiqa1e/PAgDeDM+M0ZH/97HIkgNM0aZuElaQP5Q77wPe7s82BYY/i7JqjWfSF+ZgaVsfPDHtsWJZpdzvyn9Isc43rmLki5MSHRmvFlPzFoIR+4Zz4OK00FIxRQWZVpceSBHuiokOcs0W1WcBo9aozFGJNMfUQ1bXfiS8iGEUxLKW/wSSMiiubHTgTo6IxKNII+WxRUtkTwx5xxkR+FGcjTJXzCr/cpX8CdDsJsf85DuMf2Tc2mH3IjX3ap8tqCBwn4m1NW7npB0Tn4K8BDFYsCw8uIiLwdIY8JS9klBj2MBIxlZIpD1E6y7kFJMPyWsOAJWIK3c1CzuPC7CuZUDpilPFH83bQVX9SnwrOfuPSmd7jDyyU9PlP6nf0C/jvAXOBs1JUgJ+uMYux4EvZpDhbIIeYN12QF6UIAlVUCp5ChyBmw5XlNRd5AGj2wGRrmDRK2JdhDyLjImIhSxCdrLVDzuaJrwiNO1Jat7llbAaofmEhlsAKzQWrkq6DlF7nyztl72O3EO/B/KMwzirSEqT9sKVszj28pV0PZhXEQ7zNHvhMOWprEHBMkSAP1fljE1Lo+pUROY5V8Fi14LE6tajIbxR61QJjdRsvjxXnXcUKzogodZPSPVxBHa87KeFA8VU48NkAM48VYW41wurTzoEDrNvIADsV9P5bM3/VQtHT2NyYOh1K5e5LUXjR0v70Uu4j7AbexqXcc/HP4IhLtnPiFfuqDUnS33VitBuVJO6ZTcme2Sg2fX0Cs1sxgTkNm8CK5/f3nSVF9D+fJk6j0qS473KeOK3Ik67bsDzRSyhDDUT7SijWVcNKKKZeQxmdOWnLzuk56fWL8ZmTNu2dnpNeBoguBpdSY0wswx0Y/a66lYvhpUZPDllUgVX9nLAE16CAClESJlL0pHex1A+VA4mH6ABOxMT36VMLXnUz8rqdxR5Ppv0aOUsnt9KV0VkHQ7ehSmBp4EZtB7d69dUccLtUFd7hns+pkbD6OglnAwjnYCD0EoJ6TK1l0LjtGdQ1G5dBen1hxJKUpAInnrrzCKXRAahRHIj/hFmnyszubGDWOyazwvC2Wa99G8L6nLharU41J1p6SWLMvHmM4S3UOaEAVO/kCaWXLG7asZGoVyVOXTyymvlhx37uf7sPNGDwB39/XsvIOt58QNr7c81OvXZy5Pfwll4pyZ/AW73nrNdJNmw5Ny2LL5hspbj++C+Huv6E0p78Aw==7Zpbc9o6EIB/DY854ws28EgJaXImSWmgc3KeOrItjFLbcmVx66/vSpaNbyHkgsk0TSbEWlkrWft5VyvRMUfh5jND8eKGejjoGJq36ZjnHcPQTUuDf0KyTSX9rp4KfEY8ddNOMCW/sBKqdv6SeDgp3cgpDTiJy0KXRhF2eUmGGKPr8m1zGpR7jZGPa4Kpi4K69D/i8YV6iuyxhPwSE3+R9axrqiZE2c1KkCyQR9cFkTnumCNGKU+vws0IB2LysnnxbPKvfntF2c2F1h3czr7fz36dpcountMkfwSGI/62qvup6hUKlmq+OoYdQCefHLjwxcU5dZchdHxN/awOOsmrM9mcwthgkvhWzbz9c0mzirNEcjGEG3Q93uwqq1oclkk+LUngJQKCFI6EJBzLLjw1ILgMxJg0FHliejAT94gmhNf1FcaeDjUTG6VRG4wuIw+LOdOger0gHE9j5IraNbwhIFvwMICSLp6NBMGIBpTJtuZ8jm3XBXnCGf2BCzVeb+BoWt7fCjOONxVAn7CuniMH7yqmIeZsC+2Ulvx922ZlKy2vd9CbA2XuRQF4W7VD6j3zc9U7luBC4fQcavVH2SoboMLbxJjstV6NvJpF3wLIHX4SrhCRiMOfkPh3k9HOYREqhWsCs2xoDIeUY0kj4NgGc1j3LNxrYm5g90xkt8icaZ6cud7T/uzZfBhNfEwY5dSlwRMQNvFapzqJUfQq34lCAUnkJOLf0AEUkMsLrIY4SSDQJfJu7ecSM4JFNV8IViPM15T96BgjSXAS00iiz6kCudgmf6500Mfwon0XN3tRp291rVa9aO/URJsHROhhHAfERcIVtRmhp1uIyGGKERKaHeEzU6qIYAYVx6XJD05jcTnP2fMgZDPiLDn2GkL7KYI4uNO+123Cr284pi0cqgAPHiwYBsSPoC4knie6bo3LwcmxfJrKW0glrsG6bSJ5ThKXrmTkBa+23sVhzdlK34Y8EoklYyTyHDk4bc5oWHeF75S8YwFmW1Y5lBs1wHRTaxOwwdOE3WFfeI9tm4SlfaZMrXDkUXEll4NgAOHamL9bNUJ9TGHhmHx4nhocVrs8dbUGnipzDeYaih0HKLkBShLilqcXJoRt75UpZOF/UfjHyornm2Ll+TYrbQi/z3TAdaEVlHaNRCFr81JTYq+2HXKIIWEe6JK5+ICVCEfMx/sUms1kFExvNVg+kzEcwIphVX6IJhxUDxPxghXAy4apwNMz8jIV6XOqVsXtkqqifkVRt6IonYeaIkln/tivAPbx9Dl3ciPIPeVq6TT+LwZDpYs1Nx2I9Hwuo0nJO16kPvGDe0Dd6p/aBRp/XWBLLrD7CBst+UC94rp6L/SBPa2iSGvZBzalGX+JPUrQHvyRxBr9lont1ogt7M4kNXoh6PAyr2VWIhrhClhKhNTegwugYLZvU6IpZJaDqoi/UzWobNlxhIBY3sPQG1LMbgNixtHioVWz1W2W/mvjyCeRSOiu0Vaudj6u4cwsF1eG67VouOXNt7XTW/S/eN++jx/su2GwGp01pXIHHCzNGIqSmLL9h4OHHi+9bs9+dj3tiPm5gM/ZaNJpY1vdEr9N0ciWP2roBXn6c8Q1cu0Aqc19zdF8YyDd//pw+fBl/DnZxs5l8lK0rsSrPAdD7M9z2jm6HG9imsjjnfkykgeU4HK4mHZ5oINK8eg0x+Rz45FjctuxrTaPLHXr1MS98JT8bjydQbPh5OodEHdOkB/RBKKY2gwVp4VbudMuSshTu+75SWX6KdeNnBG8ktUfDMVudTnU4hZpI4lvuT2QF56XbGXXaSvjWdnWwaZ8k2xrX/QoJluPv/PvJtcyzLKKg3dIB08oertcq3EWHz+D3Os5b9LvQkDLr0sMLd+B+9ydekeUk/mHjc5Vl9g/tUusZ/Mndom9P9UlnvTQKE9Vc/AqQB3qEvVedXlZUXRkl1jf0ShmJh94C6NyFqM1OJY32sKA4u4r4aldd1+sN8e/AQ==3Vzrd6I8Gv9rPGf3Q3u4in601s6+u307Tp3uXL68J0JE3iJxQqy6f/0mEK6JiArUaedMS0II8Fx+zyVP6Onj1e4TBuvln8iBfk9TnF1Pv+9pmqqbCv3DevZxz8BQ4w4Xew4flHXMvP9B3smvczeeA8PCQIKQT7x1sdNGQQBtUugDGKNtcdgC+cW7roELhY6ZDXyx95vnkCV/i+S1WP+/oOcukzurCj+zAslg3hEugYO2uS590tPHGCESH612Y+gz4iV0ia97OHA2fTAMA1LnAl/ZDSb79frL5POP5xeMnxdT74bP8gb8DX9h/rBkn1AAo03gQDaJ0tPvtkuPwNka2OzslvKc9i3JyqctlR46IFxGY1lj4fn+GPkI03aAAsi6UEBmfG41acdMV/u0HRKMXmFyUU/T+/YAzhfpmYQHlHp3IgGSt4GYwF2uixPkE0QrSPCeDtkVpWyfci9ubzNW94e8b5ljs5HwH3DxctOpMw7QA84EOUNm7s9vP3fTb/puNnkNvmyn45f/ShnS9+lt7+b0wGUHT1TJHr2QJCfofdJz6WBc7mGULjC2/2uDkhM3YcSDER2gqutddjKdxbwbo9WaMpHOYt6Lt7n3QhtRwod0igBuGWNg3JozemMIHC9w2UkGEdHTKwuMVkyflzC6iGwRfj38AvQ143dIui+R07xoUilzABws7Er5a0LaEu1KxM0QxU0dDERx6zcgbW9/TZ7s2cDb4/lX+w2QG3OwvtFMQdxmm3loY28OjxAYYbJELgqA/4jQmpP1b0jInusz2BBUJHpIACYjBsoZIER9Dx577GhauPPId3Z8a/LWD341O77f5Ybd73njIGtCtME2rNA2nVsUgF1IqujEGQCdgrUQOY2hD4j3VjQejYOEfhwknqFLlSx6JnYvTRmjIIRB16AxCVyP8lmGGC8hDPN6T+06Q4uNRy00pVnAfq3XvmdTgqLgI+OC3i+ZIQks6BIr1AQsfPmJvxnbF/jgTl5D53EH/+1/v9EE+WL4LbJgiykxBYIzJUlsPNfybqAjcJIRtg/C0LPjTj6k7G6oFyGHKSKHlJTqewGHHMdE5JhRCkVmmXgLrmthJUsz7k2y3q44TInwPbEHrPEjMxWsmdmHqJUYiMyo9C0tZ1aUW0W1Kk0La0wh9ij5IT5mbppjMr90irwIf3eJ16nfDgf6MP0poIbWV28VrThnLMZ8mnxsIJnZLM6mlMAllnRhKsoxsM8NW7MBYdU7GMX76GpJquMZMxlP6Xe+vRS9HMFe3iN7s6IMfUTuVfnVd8weMju5pJEV/cNdZxApLQU5ZHuAQNZw+BuEH9lUaiVTmbZztlIzrHZspfbX5MtgPrgzlyvtsz4lc938+2Z4CFFXMAyBC68WTGvZPukr90XbJ49lr8v4JdFWjlWTnb0EgQu5fkUOKdUOsGKyHv8+olnmnfs8HUeq27UblDk5edampvF73hTWs5Pnu0MJq49GUvqgG1tpDqwCUJjqoJY9a8rkqDUSOVNt+lTMenRicvgs4ZrFWJVmiD5OYdRh6xTboxWg1KT/WQ9PyMbuJIVnsoyYukIsYkiSQx/FLJUzO0aSxcmZJX1gtmOW5PJnHZS/y8Rn5DgHGckE41KxPCYMHVjPcpSYhg+F0CHJTx0IHLqH4HezynIEHpTCk3JAUTc26RfnMRWzFpBn8yQD0WIRwsuCl+KjqJZS+eSl4YkJairUkfpmotmJFPbhE3vz1JMRkTweFS8r5IbxsKOsg7JMS06BMuG/VSwrrwBqEnFXReus1VzQnYNgU5JES/oulHbNKgYmulXyN+qKO5V3SqOi5OiaGNo3579I5UhMFD3DEPlvMA/+ic8sChMznSxaRTbwz5aiNGtj1V8LyImeZRag91zgvRIBM4bRGxXF4kxIlc01qJfvaUq+DIl8sRULyFwItkbIJggWSBSufwAnn+745yXopKrWKTIiRabjDkld0aphvM1KN/SG+hd90fNsE/ZKK+hnW/l0LbSc2HkHMy+8lFFt5wVXpzi+HUMv5jSfozWaI9mKXxu4gdJ0RU00VnsnOMGdq0ZdP/mgCim3+sDod6pCJc9BTfL6J3sO1vWoUFr3JXeV21GJvqASQsrl5BhYk8WqU4wIspF/Qqb9cH6nlQWEA/cXHzTO75ydDJiHBAObSYIS5UFzae8odUrhBntRWrVU5TOOFCekDx7lj6K1f5ZLyF9zIAn122eHUl+ri+yQVFPE5BA3HhjakEKZU+HcN2FETqnuyblrHTn0mTel058ubYFuabcN+fiSqWq6+HXswRmQLa0/FcPMPwLqWgcsavz99Fwt0LuviGoureMxGlBzKXVFH/ERutHqZIm29AVJr7TSlCdUUr0rFvQC33MD2vThgs3AiOXRmH/Eu1ee40SOnoxfRY4WS4dzKVizV6wbNptnlWmJrDIknCorY2OcEl2Xya4FPViY7J9UD6IfTulcf/zTjn6Y760fohlM1+MUXrn4W2lKvxU+qUk7X2Mh41Nr2iFZTB0VkkByN+V5Mvt6jpdyUimacqud5JacldU8yNTGKpxrrwoVnKOinHTpKfW1YoBpnekmGapxO7SMQzV15Wm7dZmqKkovWdiVBrX32aJPSBCG9YNb8+4eEDAHoTQmCJdgzQ7tve9REMPHjdY8RrvHedoB7Fc3wsDPG+IzWJZB7+HdQlUeYXmxN4FpGwZRjreNAFBWtTZURV1pbd+HbHUniv+yorWTwr721+SvZDHoimvjDoSBmlEKA4W9a7UjSnGqYWtFVfIKPkFsxyC6yvFwXGvUgr+WAsEFHlsDGGIU69ksSXA5NNrxyaS8kNVUn+hnHaTKlSy5lvxgSz3bx6iep2Wt0UW0r0TwLvfs1ALaSof1eMWp1jTSXqQ3dQpDR9M/rmoTwmS3RvFuPbSGONkjFCft02KQcB8SuPpARZ1meQ3LGCb2L5+xkOWJ2tuwK9awt7TGlYQDT8WNYScvbxU8yEzMogWgp9HX2RGBy5WilbaoxeLnMeMMw3gLTCqCHywEydwb/Z4CI5uwkX2nxqAk4JKEnNnSulQlVld+/uAdSuaPAeRnbC8hWwolubVOyDOITNJt5PswXid1POAGKKSu5EfCSsMqbyCUxLiqIRGm8qpUc8IkJtXHMRvkXKiXO/ztHCXdqOkoXdl2LV2M9gQs+BMFHkG4YyzIlmaUWWRxpJjAny2MbgUiFwktBHB4j62a77sKZJYyt7KvHXTqUyVb0qo/p8G93KdNBOJPUYXq1Ujdx5UWq1SLKdtZ1drHMcL/kJ32/HWvvfz4ZczdN//heSbJnY6clRfw2uUTTMnlOdHL0ptCieUR6yKlRt0oXG0nvWmVyobVcnKzudxK1etXQQf1HFcgcG4eI7xXovqWBdO9buFjlPuWjsxcpQgHuDRj/t2da7FatR1ZAVwkkn0Qb/rlalxZSNQM3NBm9vW/WCCzbyjqk/8D7Zlbc6M2FMc/jWfaB3tAgC+Pwbn0srvJrjvt5qkjgwyqMXKF8CWffiWQAAFO7Nhk0mmcmVg6kg66/PTXQe5Z09XujsJ1+Jn4KOoBw9/1rOseAKblGPxLWPa5ZWybuSGg2JeVSsMMPyFplO2CFPso0SoyQiKG17rRI3GMPKbZIKVkq1dbkEh/6hoGqGGYeTBqWv/CPgvlKNSwhP0XhINQPdk0ZMkKqsrSkITQJ9uKybrpWVNKCMtTq90URWLy1LzYX4H7LUznf/95PxsvnjYRXaJ+7uz2lCbFECiK2WVd27nrDYxSOV89MIz4Q9w5TwQiEacs6VMU4ITRvSrljykqFC1o3ZKsYSwmju3lagz/TcVsuQsSs36SsXLFK5jmelcWFl4c9yYOcIx6znXlublT9SCg+QeUpLGPxIgNXrwNMUOzNfRE6ZbzzW0hW0U8Z4pe4CiakojQrK3lQzReeKLbjJIlqpQMvTGaL4rnvbgQcsE2iDK0q2AoF+YOkRXKJtPY6Ztlr2e3JbCmojCswDqUNij3SFA4LjngCYnCCVgMj8QiRmxL6PIwAW2cfFBxJBXAcN4XFqoDz3HxhXPBq0z5KPECe5AhnrtKWUgoZvsmKX/QNGG8isuXiK/XSfoigDmLpCmJFzhIKWSYxDpQue8DQPGjYC2SMeHDe5GoOfSWQcbgfcoigW5u9yFd3vNWYl44mgO+3B3CZNU0xm6hyWihqTBeHiezgZOiYS5p4Oc9bWHm06wHpmJJcBBDllIeYNSXCMX+lQgfeM6LYJJgT18UtMPsu1QEkX6spK/FnFmOyu1VUcxHnbUZOCr7qNyJTNauqFm2404eEMV81hCVttOWOSEp9dARO5NBGqDnHEplR74WNTWpqVDhtEChbBRFfO9s9FirDRT5hAeCsy0robRADcq6dOUDl62qwU3NERjpjgqtVI7yiWk4yrgthn0GyuAIZVSHpeHymJEfJ1wngPGFiPD4Q/Y6kj1r8v5kz2qw8muMGYai5Roh+r8XM/CfFDPHtNtRO1XMbMsZTIZG+dH9DseDybhSCt5W6Y54ZRSaVo8Bm2LmCmYo3uQh4hKd+HL5oYKvf798ByLoNDASLwlZn7KvLMYzPD7EDuRQjKuih0NHE8SqHFbU8QRBFIo0k/0llL/7BCSG0U1pdb2UbrI3UrMD/bQvrJ+SImMAhhOgkdQHTZK6jBfH9mBkjyajkW2MDTCxdKydVwquNe5AcDmhcF+pthYVksNjK3qvQlj59n+o0/X6tnb9xxN5B2qNVW/IYpGgbg6I5uXRAyV+6pVhbinmP+EkSVHy8+u3+AA4lV1uVne5tsc7iFPsdxV+2LUX/NFro48DZ8Xl4wuTWU+fHdfYP971v377/lv0+ybtN4Pjh3Qe4SQ8DZ+KAMdEHLqufglYqvInQtaSnH8QY3v5EwZMGdGRS/jImYJSOs1st1gM8UzCWifj2EBYne8vong0Y8ce7s91+4hrwluxKHkA9ra/LNzTAMb4qR77td5gd3a3vHDEX+vdcvaR46nY80+X14S1O2fLaAkV21TsEpfOrTA1A8WKIDyz7dvjrjdRAXU4qdOoEkh2cDadpxzg1Bhw6IzMyxxYjjkYjbk3G+T/28E7+vw6J7Th2fKH3Lx6+XO4dfMD \ No newline at end of file diff --git a/docs/diagrams/network infrastructure layouts.drawio b/docs/diagrams/network infrastructure layouts.drawio new file mode 100644 index 0000000000..b6104b8a19 --- /dev/null +++ b/docs/diagrams/network infrastructure layouts.drawio @@ -0,0 +1 @@ +xVfbcts2EP0azqQP7vAisvKjJflax6Na7sTpG0RCJBqIUEDQkvL1xeJKivIlHndiz9jYg8UuhHOwWAXJdL275GhTfWYFpkEcFrsgmQVxHIdZJv8BstdIJDGNlJwUBvPAgvzABrRuLSlw03MUjFFBNn0wZ3WNc9HDEOds23dbMdrPukElHgCLHNEh+oUUotLoOP7D41eYlJXNHGWnemaNrLP5JE2FCrbtQMl5kEw5Y0KP1rsppnB69lz0uotnZt3GOK7FWxawR349ir98b6/Q+SW6+ye94Z9PRjrKE6Kt+cDl/XwaQKgL+ffq4WEO8+wJcwVmVOaaLOWghMHD7cJicoGD5X+03shBvWw22lY+zQbVcBJib443+97Cx5/kjDIeJGdykpdL9ClUyWAfR0e/wRDCSj5rcbJCa0L3evma1UymyXHfpVHCAodws3NTS5R/Kzlr6+Kkv4NP8Wjs0sWjUz9OIbndtv+86eEdSCUJgCp+nWVJSRUtEpnBGIKnwEMKh/6Kb+R8LffvChP7MJp8NzPBFakLNcOx5L3BOhlnuz2MgErpPFlyZZXaWixulZvAfE1qJBj3CTTfLoHAO72mEmt1UrNImY3g7BueKiIArVmtVk1WhNIjMKKkrBVE8UpHlNsVRF7eMze1JkWhk0+2FRF4obUhZ7aSLYUrAWB9NqFOKCWjShFAscY65yWT4N2bGHFTjoxLzNZYcHWSYSeIFJYPs/f4qANvVQFyU0l3SaWLkI/XmUONx8tO/kOpKNPppQv2VWz8BnI/fullYdD33t6VuFcCYlzIgmtMxkXFSlYjeu5RRxDQk0y8zy1jMpFUTzL5FwuxN68HagULjLr0LN4R8QjLf09TY35V5qk1ZzsTXRn7jjHHnMgTk/XPYLU8vceu8bVr+EDKgkhZas3DWPos4ACerekGaljLc/xCIU/M24h4icULfpF7ebpalDedIkGe+vtA5u30mnFL54yAzsOdfdbNq2ee+XHYj6C3ZRb5NypQMrLRPaQU+BOvWDR4xe5aIUUf3kl1DgTXLxKuRNiCksxyefrAEbxXOanLByWzsQduod7IyuCRe9MCRAqr0AZy5e0Swuq3ZxYB4wXhskshDNJITqFF0NXGbC/qC9dXPth5shrnOM+DfqlUM8txOkq9pEyJelFUQxHYBWGPyygz9tb3P9HIYFWn9zkNn5dNj/GfpTcZ0Dtvl5Tkrh3htrhcA3E1FsMiQ6nsGHGXHMpambT7Kpg3oc9Ah5zw/ZL4CFr6Vyw6wkp8hJXDq/geVi5HzfSve7qaXi7iG3yzON/ur49cuiEfLyAWWGCum8spXMuVvJgCP7+s12Wm0MPNOXlSS8I/MZxWkM4G7FvOaybw65Tb+zoKDhuKZCaA91cKxUkWdFoII4DDm5zC77GbnKkfE6GD65+PkZL7TmK1lGZDMYXJ/3PFj4opHojp70Z+7fsFvcIhcY18u8QZfJNURQM1DcktfEGoazHqwjqpBlUhZv6tbcOo3zc4641tQ7dHeOnGHmkvflXbkPV1GIfpB/UNgelZO+6+W03O/wM=3Vhdc9o4FP01zHQf0jE2JvAYkjTdKc3QkJ12903YwlZjLCKLAP31vVdftrDz2aWzs8lDpCPpXlnn3qOr9KLz1e5KkHX+mae06IVBuutFF70wDIPhEP4gstdIHzCNZIKlBquBOftBDWinbVhKK2+i5LyQbO2DCS9LmkgPI0LwrT9tyQvf65pktAXME1K00a8slblGR+FpjX+kLMut5/5wrEdWxE42X1LlJOXbBhRd9qJzwbnUrdXunBZ4evZc5PrTXXw6q26uT2/l/ffq6ttnfqKNfXjNEvcJgpbyzabPsvzq6/Jy9eXy/kdMT8++7Nk/1vQDKTbmvMy3yr09QJrCeZouFzLnGS9JcVmjE8E3ZUrRTQC9es6U8zWAfQC/Uyn3JjjIRnKAcrkqzCjdMfkNl7+PY9P9W3XHtnuxM9ZVZ9/ozKhgKyqpMJjeP27a47/iG5HQJ05iYGKTiIzKJ+YNX0hG30UI5BblsEWxh3WCFkSyB39zxMR45ua5pTPOwE0Y7Gz6mWA06Tga+hb09s2iOhig0dhFDakQeUW4hK1wyW5mYCpAT2Hw8fZ2huP8AehAcFjAEU0W0MiwcTudWwwWONhi1ZqUXuwN7zeYXJOEFxz4PcPzyxbkXaCMo9/O1h/YRLOgFqU8WZIVK/Z6+YqXHNxAJHhTKhWZOCFY79zQgiR3mQruE38H78LByLkLB+O6HaNzu+36++JDhY3h0BFV6uF6loRY0QDIBbbReIznHuMhPzO37+baEH2TmbA2o8l2IxOaszJVI4ICzxXVzgTf7bGFVMLkyUKoXqZ78/lUTYM8XbGSSC5qB5pv50DSnV6jBALhvupWUvA7eq6IQLTkpVo1WbKi6IBJwbJSQQVdaouwXcngajhzQyuWptr5ZJszSec6NmBkC2wp3KobgoF2CCGjtAyhUGON8wIndPciRtyQI6NWihhzvuYjrtkL9jU+aMBbdb25oai5JNdXXG2vMQba4fCs4f8wVFTXxUsT9KPYzGuFu0t8nec2NzqumylZwCrvitBsRhcJHCBq/QGXjkngi0I2k4Wyh4m8RjFU8hhPevFFr8EfmA7dHgxvHTWJMVZXAk3xf/xabYu/sR68D8bB2NNxq6xvvR7sFL5cVvQo0h+1pP96A8caBtdAcYvFR9lp8YhqzMrsVpUKoxqYYtJCetXIjanSFGdQjK3RV7JZoNnK0ImEp0wAaYyjG7j2sYrTlJvt9f3IquUDdx4tRwlNkp6vN2pkMYoHcfBUvLy1KLBW/Ltd5Tz2t3Xd2h8YLG/UrOPg8TjxwuC1nA9anM82i4Il7mIXNo3/RDZLE3leOhcFVPq0yVjBN+C0qbdGbX1aGowFb4+To3E18Ljqd1AVdlA1OhZV8ZEqs9//GOhW59cX9MMXFvTRbyrof4neYYveKSdQkwQgqKRMkNQDpnweXpJsjTNvqfSBSKaEjpadIjlMRnSxbCnukbLw4DXk1PG5NBweKw1HLZ7+qmj1X0ijChJBnuE/VJQGk6piiYU/sMI9xcvUTlKVtELM+K88r8f/p2wct1hu34hPIBaYU6Hl+ByrpSXUS5I+vszT5RjfpzPBHtSS4BPFj8fq9jDS7K1bckmf1wFbRg06CmyJgfdM/QY61Y68wwIrxt9O7VA/xkID1z9HvMwPdaSz8gqif6Xy6pnnUqM4rx9K0eVP7Vlbe9o4EP01PJLPFwzmkUvYtCVZtrCb5IlP2LKtrbBYWU5Mf/2ObPkWm0BIafPQ9KHWsTSS58wczSQdc7JN/uBoF9wyF9OOoblJx5x2DMPQ+n34TyL7DNEByxCfE1dhJbAk37EC82kxcXFUmygYo4Ls6qDDwhA7ooYhztlzfZrHaH3XHfJxA1g6iDbRe+KKIENtY1DiN5j4Qb6z3h9mb7Yon6y+JAqQy54rkHndMSecMZE9bZMJptJ7uV++rcmCB9do/XkQ/LOeI+v2sdfNjM3esqT4BI5DcbZp/26BJ3+NZ+OluRzce7d0PuNqifaEaKz8pb5V7HMHYhf8qYaMi4D5LET0ukTHnMWhi+U2GozKOXPGdgDqAP6Lhdir4ECxYAAFYkvVW5wQ8SCXX1mWGj6mw2E+nCbKejrYVwYLzMkWC8wVlp1fHrrGf8Ri7uBXPGGq2ETcx+KVedaJZOhFhEBuYQZH5HtYxzFFgjzVD4dUjPvFvGLpghHYxtCSPP162RKVjna/biE7vlpUBgM8VE5RQmmIvCFc9Ea4+F8XYEqTOxnazWq1kO/ZE9AhwT4FF4038ODLh9V8mWOwoIBbQ26ONiBFtTBBlPghPDvgecn3GLYRBFJ9pF5sietmEYkj8h1tUnsyKHbSIamLrHHHmgLisVCoeNSN4gzSIE46LbqkjJVqUA2Aw6nVDABlXbvShtqwxmVX5d65IZJPYZ4X4ffSv3/8PN256z9nX6bwLWv3foVoC/0LztzYAX3/BarRTmEj80/L1BMUwmgqRKuXDhB/Mq011t6aoUaDorsYIt/Q7uBqb7B0MIEaqRbtkENCf5VyY5fAHHvSF0aJfFWXacoJ3Jk7uZcTb6TZSNElGXcJh7wiTG4DvpeXbUapOp7+gm1C6YRRxtOTm57tYMeRJgVn33Dlzca2epb2Wkqfq925Fa2Wtoalxs9leaH3FBZUSouhdiHOzWZaxhtKnEJ/ea60nySboRKHWrpSCgUZrjJGWQybjp8DIvASqJXgMxSJLUmo7Gjnx8nFuKpfl3oLVUYLVfalqOq11FsvLskFRSRsvya1ttv2wwrvUUG1Tiy5jJ9Ucr2LWes4s8vlHEynRZAGlzRlyJV1UhvTZeJWuO3/F8teI/V/N5PSEUzQjV1SvixsWDJgbkZwQyf7ImbufBImcmRNG5FTj4tT8r4SA40L44VeuwjbXqte9x0bb7wLaoBR1+tCm4+JQP9SIjBohMrf0ccoocDNfP+Qd2VykPVhhpWPy0YsHVU7MbmuC3UtlLXVHm5g5+PTm7gINEGMZPef3kQoioiTwzNCi74xdPNJIQtxhqj37+kF7ROF6Wf1gu+KNvu4MDULhSayxDzr6iayZvSgahT48LqamqVKtODkKV2ifcH7AwqU1x4hE/i4BOXFZK+lExQyAY5Usd1+Swa8LDMt+a9VttIfZaGCZz+XlDOzrmd2ryln+oXKz9Z+p9lyjKCah09MY0T2Hg4lOG1RHfBGdAUPcQQ0fGTFG5yqePnzY1Xb5JKe/euV7pze93BPe6oiHgxduB0sbVAvybvmh/+1R9svSc9Q0BWPIxEJxt+kmw/gsqFKnUJ2tQ2kAcW/ZfTHyWhrG/+DdBSG5Z8Jsqgs/9piXv8P \ No newline at end of file diff --git a/docs/pages/configuration/options.rst b/docs/pages/configuration/options.rst index 603e825142..d03a63a2ad 100755 --- a/docs/pages/configuration/options.rst +++ b/docs/pages/configuration/options.rst @@ -36,15 +36,14 @@ events.natsPort 4222 events.purgeCompleted false Purge completed events at startup events.retryInterval 60 Retry delay in seconds for reconnecting **Network** -network.address Interface and port for http server to bind to, defaults to global Nuts address. -network.bootstrapNodes Space-separated list of bootstrap nodes (`:`) which the node initially connect to. +network.bootstrapNodes Space-separated list of bootstrap nodes (`:`) which the node initially connect to. network.certFile PEM file containing the certificate this node will identify itself with to other nodes. If not set, the Nuts node will attempt to load a TLS certificate from the crypto module. -network.certKeyFile PEM file containing the key belonging to this node's certificate. If not set, the Nuts node will attempt to load a TLS certificate from the crypto module. -network.grpcAddr \:5555 Local address for gRPC to listen on. -network.mode server or client, when client it uses the HttpClient -network.nodeID Instance ID of this node under which the public address is registered on the nodelist. If not set, the Nuts node's identity will be used. -network.publicAddr Public address (of this node) other nodes can use to connect to it. If set, it is registered on the nodelist. -network.storageConnectionString file:network.db SQLite3 connection string to the database where the network should persist its documents. +network.certKeyFile PEM file containing the key belonging to this node's certificate. If not set, the Nuts node will attempt to load a TLS certificate from the crypto module. +network.grpcAddr \:5555 Local address for gRPC to listen on. +network.publicAddr Public address (of this node) other nodes can use to connect to it. If set, it is registered on the nodelist. +network.databaseFile network.db Path to BBolt database file for storage of the network. +network.trustStoreFile PEM file containing the trusted CA certificates for authenticating remote gRPC servers. +network.advertHashesInterval 2000 Interval (in milliseconds) that specifies how often the node should broadcast its last hashes to other nodes. **VDR** vdr.clientTimeout 10 Time-out for the client in seconds (e.g. when using the CLI), default: 10 vdr.datadir ./data Location of data files, default: ./data diff --git a/docs/pages/development.rst b/docs/pages/development.rst index 6c6c7328cf..a412b44b55 100644 --- a/docs/pages/development.rst +++ b/docs/pages/development.rst @@ -8,7 +8,7 @@ Nuts node development Dependencies ************ -This projects is using go modules, so version > 1.12 is recommended. 1.10 would be a minimum. +Go >= 1.15 is required. Running tests ************* @@ -24,20 +24,22 @@ Building Just use ``go build``. -The server and client API is generated from the open-api spec: +Code Generation +*************** -.. code-block:: shell +Code generation is used for generating mocks, OpenAPI client- and servers and gRPC services. To regenerate the code +run the `run-generators` target from the Makefile: - make gen-api +.. code-block:: shell -Generating Mocks -**************** + make run-generators -These mocks are used by other modules +The peer-to-peer API uses gRPC. To generate Go code from the protobuf specs you need the `protoc-gen-go` package: .. code-block:: shell - make gen-mocks + go get -u github.com/golang/protobuf/protoc-gen-go + README ****** diff --git a/generate_readme.sh b/generate_readme.sh index 686af9cfea..b8c741e4a5 100755 --- a/generate_readme.sh +++ b/generate_readme.sh @@ -1 +1 @@ -rst_include include -s README_template.rst -t README.rst \ No newline at end of file +rst_include include README_template.rst README.rst \ No newline at end of file diff --git a/go.mod b/go.mod index 7dc70828fb..86fec21c79 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,12 @@ go 1.15 require ( github.com/deepmap/oapi-codegen v1.4.2 github.com/golang/mock v1.4.4 + github.com/golang/protobuf v1.4.3 + github.com/google/uuid v1.1.2 github.com/labstack/echo/v4 v4.1.17 github.com/lestrrat-go/jwx v1.0.7 github.com/magiconair/properties v1.8.4 github.com/nuts-foundation/go-did v0.0.0-20210125091337-21da6ad69a82 - github.com/nuts-foundation/nuts-network v0.16.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.7.1 github.com/shengdoushi/base58 v1.0.0 @@ -18,4 +19,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.6.1 + go.etcd.io/bbolt v1.3.5 + google.golang.org/grpc v1.33.1 + google.golang.org/protobuf v1.25.0 ) diff --git a/go.sum b/go.sum index 7a947f419f..0b16dec979 100644 --- a/go.sum +++ b/go.sum @@ -1,148 +1,84 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/spanner v1.2.0/go.mod h1:LfwGAsK42Yz8IeLsd/oagGFBqTXt3xVWtm8/KD2vrEI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= -github.com/containerd/containerd v1.3.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= -github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deepmap/oapi-codegen v1.4.1/go.mod h1:1jY0YDxfBF3tXk1u3sARJMSUJa9wV0UrVT6o+2mr/zQ= github.com/deepmap/oapi-codegen v1.4.2 h1:boVMuW+o0sOnEDB8oWRobBm1BrD5d9bUtIQVcjwMsSk= github.com/deepmap/oapi-codegen v1.4.2/go.mod h1:1jY0YDxfBF3tXk1u3sARJMSUJa9wV0UrVT6o+2mr/zQ= -github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dhui/dktest v0.3.2/go.mod h1:l1/ib23a/CmxAe7yixtrYPc8Iy90Zy2udyaHINM5p58= -github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v1.4.2-0.20200213202729-31a86c4ab209/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/getkin/kin-openapi v0.26.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= -github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang-migrate/migrate/v4 v4.11.0 h1:uqtd0ysK5WyBQ/T1K2uDIooJV0o2Obt6uPwP062DupQ= -github.com/golang-migrate/migrate/v4 v4.11.0/go.mod h1:nqbpDbckcYjsCD5I8q5+NI9Tkk7SVcmaF40Ax1eAWhg= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -156,11 +92,11 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -169,30 +105,24 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -202,8 +132,6 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -218,61 +146,22 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.3.2/go.mod h1:LvCquS3HbBKwgl7KbX9KyqEIumJAbm1UMcTvGaIf3bM= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM= -github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= -github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= @@ -295,38 +184,23 @@ github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d h1:aEZT3f1GGg5R github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8= github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087 h1:T5Wh8C/p5nWoGuEUBQj+daEXkj1CScB9GshvvsBJhpg= github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= -github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -343,34 +217,15 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= -github.com/neo4j-drivers/gobolt v1.7.4/go.mod h1:O9AUbip4Dgre+CD3p40dnMD4a4r52QBIfblg5k7CTbE= -github.com/neo4j/neo4j-go-driver v1.7.4/go.mod h1:aPO0vVr+WnhEJne+FgFjfsjzAnssPFLucHgGZ76Zb/U= github.com/nuts-foundation/go-did v0.0.0-20210125091337-21da6ad69a82 h1:AUj8bNaFcd2mR1+38hIrJU4oOs8Y7YDSElPWS+srTF8= github.com/nuts-foundation/go-did v0.0.0-20210125091337-21da6ad69a82/go.mod h1:wpgiNMle93QRhzm2Oh4J+yGgbSpw1LoBAcsoUqNr2Bo= -github.com/nuts-foundation/nuts-crypto v0.16.0 h1:PzNDcoawuGxl4KRoD3nKnz+1sFiMbEG/S95mYbbso6k= -github.com/nuts-foundation/nuts-crypto v0.16.0/go.mod h1:NS4akgWLGO8RwtzMj2gXDEpucV0S6QDeyLxEkzGSyd4= -github.com/nuts-foundation/nuts-go-core v0.16.0 h1:bHKH0eREWecXLWUexC676RACsLdz/B2tuhkNFzi21FM= -github.com/nuts-foundation/nuts-go-core v0.16.0/go.mod h1:biWHwDgHorQ6diimNbfFFqM/bub27g5/a6IZdUdojS4= -github.com/nuts-foundation/nuts-go-test v0.16.0 h1:jfbh2z44FAsRSPXquTZWviOlB9xb9YeJDzBfPMR5Xz0= -github.com/nuts-foundation/nuts-go-test v0.16.0/go.mod h1:aZ5Urj7v1lH8AD2TIejEiPhRX8t6YG9PVXhuRyNIAII= -github.com/nuts-foundation/nuts-network v0.16.0 h1:JtSuHXoqB2rLHhTDBocyqDaS11mCpp/V32nF+XEbEk8= -github.com/nuts-foundation/nuts-network v0.16.0/go.mod h1:oKrpBUpKoKSQRbEaHkp58yKEnYHkxb75CbJUXuVJSMg= github.com/ockam-network/did v0.1.3 h1:qJGdccOV4bLfsS/eFM+Aj+CdCRJKNMxbmJevQclw44k= github.com/ockam-network/did v0.1.3/go.mod h1:ZsbTIuVGt8OrQEbqWrSztUISN4joeMabdsinbLubbzw= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -380,51 +235,38 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs= github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -436,17 +278,12 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -455,7 +292,6 @@ github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -464,7 +300,6 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -473,40 +308,25 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= -github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= -github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -517,11 +337,6 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200213203834-85f925bdd4d0/go.mod h1:IX6Eufr4L0ErOUlzqX/aFlHqsiKZRbV42Kb69e9VsTE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -531,26 +346,18 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -560,24 +367,13 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200927032502-5d4f70055728 h1:5wtQIAulKU5AbLQOkjxl32UufnIOqgBX72pS0AV14H0= -golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -587,45 +383,31 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200928205150-006507a75852 h1:sXxgOAXy8JwHhZnPuItAlUtwIlxrlEqi28mKhUR+zZY= -golang.org/x/sys v0.0.0-20200928205150-006507a75852/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -634,9 +416,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -644,137 +424,87 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200128002243-345141a36859/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200213224642-88e652f7a869/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200507105951-43844f6eee31 h1:Bz1qTn2YRWV+9OKJtxHJiQKCiXIdf+kwuKXdt9cBxyU= -google.golang.org/genproto v0.0.0-20200507105951-43844f6eee31/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= -google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= -modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= -modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= -modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= -modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= -modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/makefile b/makefile index 2adf167ff9..8bde2a8058 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,6 @@ .PHONY: test run-generators update-docs -run-generators: gen-readme gen-mocks gen-api +run-generators: gen-readme gen-mocks gen-api gen-protobuf gen-readme: ./generate_readme.sh @@ -8,10 +8,19 @@ gen-readme: gen-mocks: mockgen -destination=crypto/mock.go -package=crypto -source=crypto/interface.go -self_package github.com/nuts-foundation/nuts-node/crypto mockgen -destination=vdr/types/mock.go -package=types -source=vdr/types/interface.go -self_package github.com/nuts-foundation/nuts-node/vdr/types --imports did=github.com/nuts-foundation/go-did + mockgen -destination=network/proto/mock.go -package=proto -source=network/proto/interface.go Protocol + mockgen -destination=network/p2p/mock.go -package=p2p -source=network/p2p/interface.go P2PNetwork + mockgen -destination=network/mock.go -package=network -source=network/interface.go + mockgen -destination=network/dag/mock.go -package=dag -source=network/dag/dag.go DAG PayloadStore + mockgen -destination=core/mock.go -package=core -source=core/engine.go EchoServer gen-api: oapi-codegen -generate types,server,client -package v1 docs/_static/crypto/v1.yaml > crypto/api/v1/generated.go oapi-codegen -generate types,server,client,skip-prune -package v1 -exclude-schemas DIDDocument,DIDDocumentMetadata,Service,VerificationMethod docs/_static/vdr/v1.yaml > vdr/api/v1/generated.go + oapi-codegen -generate types,server,client -package v1 docs/_static/network/v1.yaml > network/api/v1/generated.go + +gen-protobuf: + protoc -I network network/transport/network.proto --go_out=plugins=grpc,paths=source_relative:network gen-docs: go run ./docs diff --git a/network/api/v1/api.go b/network/api/v1/api.go new file mode 100644 index 0000000000..598672f88c --- /dev/null +++ b/network/api/v1/api.go @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package v1 + +import ( + "github.com/labstack/echo/v4" + hash2 "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network" + "github.com/nuts-foundation/nuts-node/network/log" + "net/http" +) + +// ServerImplementation returns a erver API implementation that uses the given network. +func ServerImplementation(network network.Network) ServerInterface { + return &wrapper{Service: network} +} + +type wrapper struct { + Service network.Network +} + +// ListDocuments lists all documents +func (a wrapper) ListDocuments(ctx echo.Context) error { + documents, err := a.Service.ListDocuments() + if err != nil { + log.Logger().Errorf("Error while listing documents: %v", err) + return ctx.String(http.StatusInternalServerError, err.Error()) + } + results := make([]string, len(documents)) + for i, document := range documents { + results[i] = string(document.Data()) + } + return ctx.JSON(http.StatusOK, results) +} + +// GetDocument returns a specific document +func (a wrapper) GetDocument(ctx echo.Context, hashAsString string) error { + hash, err := hash2.ParseHex(hashAsString) + if err != nil { + return ctx.String(http.StatusBadRequest, err.Error()) + } + document, err := a.Service.GetDocument(hash) + if err != nil { + log.Logger().Errorf("Error while retrieving document (hash=%s): %v", hash, err) + return ctx.String(http.StatusInternalServerError, err.Error()) + } + if document == nil { + return ctx.String(http.StatusNotFound, "document not found") + } + ctx.Response().Header().Set(echo.HeaderContentType, "application/jose") + ctx.Response().WriteHeader(http.StatusOK) + _, err = ctx.Response().Writer.Write(document.Data()) + return err +} + +// GetDocumentPayload returns the payload of a specific document +func (a wrapper) GetDocumentPayload(ctx echo.Context, hashAsString string) error { + hash, err := hash2.ParseHex(hashAsString) + if err != nil { + return ctx.String(http.StatusBadRequest, err.Error()) + } + data, err := a.Service.GetDocumentPayload(hash) + if err != nil { + return ctx.String(http.StatusInternalServerError, err.Error()) + } + if data == nil { + return ctx.String(http.StatusNotFound, "document or contents not found") + } + ctx.Response().Header().Set(echo.HeaderContentType, "application/octet-stream") + ctx.Response().WriteHeader(http.StatusOK) + _, err = ctx.Response().Writer.Write(data) + return err +} diff --git a/network/api/v1/api_test.go b/network/api/v1/api_test.go new file mode 100644 index 0000000000..1e90a41a29 --- /dev/null +++ b/network/api/v1/api_test.go @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package v1 + +import ( + "github.com/golang/mock/gomock" + "github.com/labstack/echo/v4" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network" + "github.com/nuts-foundation/nuts-node/network/dag" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +var payload = []byte("Hello, World!") + +func TestApiWrapper_GetDocument(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + document := dag.CreateTestDocument(1) + path := "/document/:ref" + + t.Run("ok", func(t *testing.T) { + var networkClient = network.NewMockNetwork(mockCtrl) + e, wrapper := initMockEcho(networkClient) + networkClient.EXPECT().GetDocument(hash.EqHash(document.Ref())).Return(document, nil) + + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath(path) + c.SetParamNames("ref") + c.SetParamValues(document.Ref().String()) + + err := wrapper.GetDocument(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "application/jose", rec.Header().Get("Content-Type")) + assert.Equal(t, string(document.Data()), rec.Body.String()) + }) + t.Run("invalid hash", func(t *testing.T) { + var networkClient = network.NewMockNetwork(mockCtrl) + e, wrapper := initMockEcho(networkClient) + + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath(path) + c.SetParamNames("ref") + c.SetParamValues("1234") + + err := wrapper.GetDocument(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.Equal(t, "incorrect hash length (2)", rec.Body.String()) + }) + t.Run("not found", func(t *testing.T) { + var networkClient = network.NewMockNetwork(mockCtrl) + e, wrapper := initMockEcho(networkClient) + networkClient.EXPECT().GetDocument(gomock.Any()).Return(nil, nil) + + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath(path) + c.SetParamNames("ref") + c.SetParamValues(document.Ref().String()) + + err := wrapper.GetDocument(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Equal(t, "document not found", rec.Body.String()) + }) +} + +func TestApiWrapper_GetDocumentPayload(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + document := dag.CreateTestDocument(1) + path := "/document/:ref/payload" + + t.Run("ok", func(t *testing.T) { + var networkClient = network.NewMockNetwork(mockCtrl) + e, wrapper := initMockEcho(networkClient) + networkClient.EXPECT().GetDocumentPayload(hash.EqHash(document.Ref())).Return(payload, nil) + + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath(path) + c.SetParamNames("ref") + c.SetParamValues(document.Ref().String()) + + err := wrapper.GetDocumentPayload(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, string(payload), rec.Body.String()) + }) + t.Run("not found", func(t *testing.T) { + var networkClient = network.NewMockNetwork(mockCtrl) + e, wrapper := initMockEcho(networkClient) + networkClient.EXPECT().GetDocumentPayload(gomock.Any()).Return(nil, nil) + + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath(path) + c.SetParamNames("ref") + c.SetParamValues(document.Ref().String()) + + err := wrapper.GetDocumentPayload(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Equal(t, "document or contents not found", rec.Body.String()) + }) + t.Run("invalid hash", func(t *testing.T) { + var networkClient = network.NewMockNetwork(mockCtrl) + e, wrapper := initMockEcho(networkClient) + + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath(path) + c.SetParamNames("ref") + c.SetParamValues("1234") + + err := wrapper.GetDocumentPayload(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.Equal(t, "incorrect hash length (2)", rec.Body.String()) + }) +} + +func TestApiWrapper_ListDocuments(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + document := dag.CreateTestDocument(1) + + t.Run("list documents", func(t *testing.T) { + t.Run("200", func(t *testing.T) { + var networkClient = network.NewMockNetwork(mockCtrl) + e, wrapper := initMockEcho(networkClient) + networkClient.EXPECT().ListDocuments().Return([]dag.Document{document}, nil) + + req := httptest.NewRequest(echo.GET, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath("/document") + + err := wrapper.ListDocuments(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, `["`+string(document.Data())+`"]`, strings.TrimSpace(rec.Body.String())) + }) + }) +} + +func initMockEcho(networkClient *network.MockNetwork) (*echo.Echo, *ServerInterfaceWrapper) { + e := echo.New() + stub := wrapper{Service: networkClient} + wrapper := &ServerInterfaceWrapper{ + Handler: stub, + } + return e, wrapper +} \ No newline at end of file diff --git a/network/api/v1/client.go b/network/api/v1/client.go new file mode 100644 index 0000000000..7b30d08d91 --- /dev/null +++ b/network/api/v1/client.go @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + "io/ioutil" + "net/http" + "strings" + "time" +) + +// HTTPClient holds the server address and other basic settings for the http client +type HTTPClient struct { + ServerAddress string + Timeout time.Duration +} + +// GetDocumentPayload retrieves the document payload for the given document. If the document or payload is not found +// nil is returned. +func (hb HTTPClient) GetDocumentPayload(documentRef hash.SHA256Hash) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) + defer cancel() + res, err := hb.client().GetDocumentPayload(ctx, documentRef.String()) + if err != nil { + return nil, err + } + if res.StatusCode == http.StatusNotFound { + return nil, nil + } + if err := testResponseCode(http.StatusOK, res); err != nil { + return nil, err + } + return ioutil.ReadAll(res.Body) +} + +// GetDocument retrieves the document for the given reference. If the document is not known, an error is returned. +func (hb HTTPClient) GetDocument(documentRef hash.SHA256Hash) (dag.Document, error) { + ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) + defer cancel() + res, err := hb.client().GetDocument(ctx, documentRef.String()) + if err != nil { + return nil, err + } + return testAndParseDocumentResponse(res) +} + +// ListDocuments returns all documents known to this network instance. +func (hb HTTPClient) ListDocuments() ([]dag.Document, error) { + ctx, cancel := context.WithTimeout(context.Background(), hb.Timeout) + defer cancel() + res, err := hb.client().ListDocuments(ctx) + if err != nil { + return nil, err + } + if err := testResponseCode(http.StatusOK, res); err != nil { + return nil, err + } + responseData, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + unparsedDocuments := make([]string, 0) + if err = json.Unmarshal(responseData, &unparsedDocuments); err != nil { + return nil, err + } + documents := make([]dag.Document, 0) + for _, unparsedDocument := range unparsedDocuments { + document, err := dag.ParseDocument([]byte(unparsedDocument)) + if err != nil { + return nil, err + } + documents = append(documents, document) + } + + return documents, nil +} + +func (hb HTTPClient) client() ClientInterface { + url := hb.ServerAddress + if !strings.Contains(url, "http") { + url = fmt.Sprintf("http://%v", hb.ServerAddress) + } + + response, err := NewClientWithResponses(url) + if err != nil { + panic(err) + } + return response +} + +func testAndParseDocumentResponse(response *http.Response) (dag.Document, error) { + if response.StatusCode == http.StatusNotFound { + return nil, nil + } + if err := testResponseCode(http.StatusOK, response); err != nil { + return nil, err + } + responseData, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + return dag.ParseDocument(responseData) +} + +func testResponseCode(expectedStatusCode int, response *http.Response) error { + if response.StatusCode != expectedStatusCode { + responseData, _ := ioutil.ReadAll(response.Body) + return fmt.Errorf("network returned HTTP %d (expected: %d), response: %s", + response.StatusCode, expectedStatusCode, string(responseData)) + } + return nil +} diff --git a/network/api/v1/client_test.go b/network/api/v1/client_test.go new file mode 100644 index 0000000000..59fda55aca --- /dev/null +++ b/network/api/v1/client_test.go @@ -0,0 +1,107 @@ +/* +* Copyright (C) 2020. Nuts community +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* + */ + +package v1 + +import ( + "encoding/json" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +type handler struct { + statusCode int + responseData []byte +} + +func (h handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(h.statusCode) + writer.Write(h.responseData) +} + +func TestHttpClient_ListDocuments(t *testing.T) { + t.Run("200", func(t *testing.T) { + expected := dag.CreateTestDocument(1) + data, _ := json.Marshal([]string{string(expected.Data())}) + s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: data}) + httpClient := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} + actual, err := httpClient.ListDocuments() + if !assert.NoError(t, err) { + return + } + if !assert.Len(t, actual, 1) { + return + } + assert.Equal(t, expected, actual[0]) + }) +} + +func TestHttpClient_GetDocument(t *testing.T) { + t.Run("200", func(t *testing.T) { + expected := dag.CreateTestDocument(1) + s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: expected.Data()}) + httpClient := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} + actual, err := httpClient.GetDocument(expected.Ref()) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + t.Run("not found (404)", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusNotFound}) + httpClient := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} + actual, err := httpClient.GetDocument(hash.EmptyHash()) + assert.NoError(t, err) + assert.Nil(t, actual) + }) + t.Run("server error (500)", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusInternalServerError}) + httpClient := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} + actual, err := httpClient.GetDocument(hash.EmptyHash()) + assert.Error(t, err) + assert.Nil(t, actual) + }) +} + +func TestHttpClient_GetDocumentPayload(t *testing.T) { + t.Run("200", func(t *testing.T) { + expected := []byte{5, 4, 3, 2, 1} + s := httptest.NewServer(handler{statusCode: http.StatusOK, responseData: expected}) + httpClient := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} + actual, err := httpClient.GetDocumentPayload(hash.SHA256Sum([]byte{1, 2, 3})) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + t.Run("not found (404)", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusNotFound}) + httpClient := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} + actual, err := httpClient.GetDocumentPayload(hash.EmptyHash()) + assert.NoError(t, err) + assert.Nil(t, actual) + }) + t.Run("server error (500)", func(t *testing.T) { + s := httptest.NewServer(handler{statusCode: http.StatusInternalServerError}) + httpClient := HTTPClient{ServerAddress: s.URL, Timeout: time.Second} + actual, err := httpClient.GetDocumentPayload(hash.EmptyHash()) + assert.Error(t, err) + assert.Nil(t, actual) + }) +} diff --git a/network/api/v1/generated.go b/network/api/v1/generated.go new file mode 100644 index 0000000000..1d386ab251 --- /dev/null +++ b/network/api/v1/generated.go @@ -0,0 +1,528 @@ +// Package v1 provides primitives to interact the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/deepmap/oapi-codegen/pkg/runtime" + "github.com/labstack/echo/v4" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A callback for modifying requests which are generated before sending over + // the network. + RequestEditor RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = http.DefaultClient + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditor = fn + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // ListDocuments request + ListDocuments(ctx context.Context) (*http.Response, error) + + // GetDocument request + GetDocument(ctx context.Context, ref string) (*http.Response, error) + + // GetDocumentPayload request + GetDocumentPayload(ctx context.Context, ref string) (*http.Response, error) +} + +func (c *Client) ListDocuments(ctx context.Context) (*http.Response, error) { + req, err := NewListDocumentsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if c.RequestEditor != nil { + err = c.RequestEditor(ctx, req) + if err != nil { + return nil, err + } + } + return c.Client.Do(req) +} + +func (c *Client) GetDocument(ctx context.Context, ref string) (*http.Response, error) { + req, err := NewGetDocumentRequest(c.Server, ref) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if c.RequestEditor != nil { + err = c.RequestEditor(ctx, req) + if err != nil { + return nil, err + } + } + return c.Client.Do(req) +} + +func (c *Client) GetDocumentPayload(ctx context.Context, ref string) (*http.Response, error) { + req, err := NewGetDocumentPayloadRequest(c.Server, ref) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if c.RequestEditor != nil { + err = c.RequestEditor(ctx, req) + if err != nil { + return nil, err + } + } + return c.Client.Do(req) +} + +// NewListDocumentsRequest generates requests for ListDocuments +func NewListDocumentsRequest(server string) (*http.Request, error) { + var err error + + queryUrl, err := url.Parse(server) + if err != nil { + return nil, err + } + + basePath := fmt.Sprintf("/api/document") + if basePath[0] == '/' { + basePath = basePath[1:] + } + + queryUrl, err = queryUrl.Parse(basePath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryUrl.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetDocumentRequest generates requests for GetDocument +func NewGetDocumentRequest(server string, ref string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParam("simple", false, "ref", ref) + if err != nil { + return nil, err + } + + queryUrl, err := url.Parse(server) + if err != nil { + return nil, err + } + + basePath := fmt.Sprintf("/api/document/%s", pathParam0) + if basePath[0] == '/' { + basePath = basePath[1:] + } + + queryUrl, err = queryUrl.Parse(basePath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryUrl.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetDocumentPayloadRequest generates requests for GetDocumentPayload +func NewGetDocumentPayloadRequest(server string, ref string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParam("simple", false, "ref", ref) + if err != nil { + return nil, err + } + + queryUrl, err := url.Parse(server) + if err != nil { + return nil, err + } + + basePath := fmt.Sprintf("/api/document/%s/payload", pathParam0) + if basePath[0] == '/' { + basePath = basePath[1:] + } + + queryUrl, err = queryUrl.Parse(basePath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryUrl.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // ListDocuments request + ListDocumentsWithResponse(ctx context.Context) (*ListDocumentsResponse, error) + + // GetDocument request + GetDocumentWithResponse(ctx context.Context, ref string) (*GetDocumentResponse, error) + + // GetDocumentPayload request + GetDocumentPayloadWithResponse(ctx context.Context, ref string) (*GetDocumentPayloadResponse, error) +} + +type ListDocumentsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *[]string +} + +// Status returns HTTPResponse.Status +func (r ListDocumentsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListDocumentsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetDocumentResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r GetDocumentResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetDocumentResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetDocumentPayloadResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r GetDocumentPayloadResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetDocumentPayloadResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ListDocumentsWithResponse request returning *ListDocumentsResponse +func (c *ClientWithResponses) ListDocumentsWithResponse(ctx context.Context) (*ListDocumentsResponse, error) { + rsp, err := c.ListDocuments(ctx) + if err != nil { + return nil, err + } + return ParseListDocumentsResponse(rsp) +} + +// GetDocumentWithResponse request returning *GetDocumentResponse +func (c *ClientWithResponses) GetDocumentWithResponse(ctx context.Context, ref string) (*GetDocumentResponse, error) { + rsp, err := c.GetDocument(ctx, ref) + if err != nil { + return nil, err + } + return ParseGetDocumentResponse(rsp) +} + +// GetDocumentPayloadWithResponse request returning *GetDocumentPayloadResponse +func (c *ClientWithResponses) GetDocumentPayloadWithResponse(ctx context.Context, ref string) (*GetDocumentPayloadResponse, error) { + rsp, err := c.GetDocumentPayload(ctx, ref) + if err != nil { + return nil, err + } + return ParseGetDocumentPayloadResponse(rsp) +} + +// ParseListDocumentsResponse parses an HTTP response from a ListDocumentsWithResponse call +func ParseListDocumentsResponse(rsp *http.Response) (*ListDocumentsResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &ListDocumentsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest []string + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetDocumentResponse parses an HTTP response from a GetDocumentWithResponse call +func ParseGetDocumentResponse(rsp *http.Response) (*GetDocumentResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &GetDocumentResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + } + + return response, nil +} + +// ParseGetDocumentPayloadResponse parses an HTTP response from a GetDocumentPayloadWithResponse call +func ParseGetDocumentPayloadResponse(rsp *http.Response) (*GetDocumentPayloadResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &GetDocumentPayloadResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + } + + return response, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Lists the documents on the DAG + // (GET /api/document) + ListDocuments(ctx echo.Context) error + // Retrieves a document + // (GET /api/document/{ref}) + GetDocument(ctx echo.Context, ref string) error + // Gets the document payload + // (GET /api/document/{ref}/payload) + GetDocumentPayload(ctx echo.Context, ref string) error +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// ListDocuments converts echo context to params. +func (w *ServerInterfaceWrapper) ListDocuments(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.ListDocuments(ctx) + return err +} + +// GetDocument converts echo context to params. +func (w *ServerInterfaceWrapper) GetDocument(ctx echo.Context) error { + var err error + // ------------- Path parameter "ref" ------------- + var ref string + + err = runtime.BindStyledParameter("simple", false, "ref", ctx.Param("ref"), &ref) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter ref: %s", err)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetDocument(ctx, ref) + return err +} + +// GetDocumentPayload converts echo context to params. +func (w *ServerInterfaceWrapper) GetDocumentPayload(ctx echo.Context) error { + var err error + // ------------- Path parameter "ref" ------------- + var ref string + + err = runtime.BindStyledParameter("simple", false, "ref", ctx.Param("ref"), &ref) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter ref: %s", err)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetDocumentPayload(ctx, ref) + return err +} + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// Registers handlers, and prepends BaseURL to the paths, so that the paths +// can be served under a prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.GET(baseURL+"/api/document", wrapper.ListDocuments) + router.GET(baseURL+"/api/document/:ref", wrapper.GetDocument) + router.GET(baseURL+"/api/document/:ref/payload", wrapper.GetDocumentPayload) + +} + diff --git a/network/config.go b/network/config.go new file mode 100644 index 0000000000..631991a301 --- /dev/null +++ b/network/config.go @@ -0,0 +1,60 @@ +package network + +import ( + "crypto/x509" + "fmt" + "io/ioutil" + "strings" +) + +// Config holds the config for Network +type Config struct { + // Socket address for gRPC to listen on + GrpcAddr string + DatabaseFile string + // EnableTLS specifies whether to enable TLS for incoming connections. + EnableTLS bool + // Public address of this nodes other nodes can use to connect to this node. + PublicAddr string + BootstrapNodes string + CertFile string + CertKeyFile string + TrustStoreFile string + + // AdvertHashesInterval specifies how often (in milliseconds) the node should broadcasts its last hashes so + // other nodes can compare and synchronize. + AdvertHashesInterval int +} + +// DefaultConfig returns the default NetworkEngine configuration. +func DefaultConfig() Config { + return Config{ + GrpcAddr: ":5555", + DatabaseFile: "network.db", + EnableTLS: true, + AdvertHashesInterval: 2000, + } +} + +func (c Config) parseBootstrapNodes() []string { + var result []string + for _, addr := range strings.Split(c.BootstrapNodes, " ") { + trimmedAddr := strings.TrimSpace(addr) + if trimmedAddr != "" { + result = append(result, trimmedAddr) + } + } + return result +} + +func (c Config) loadTrustStore() (*x509.CertPool, error) { + trustStore := x509.NewCertPool() + data, err := ioutil.ReadFile(c.TrustStoreFile) + if err != nil { + return nil, fmt.Errorf("unable to read trust store (file=%s): %w", c.TrustStoreFile, err) + } + if ok := trustStore.AppendCertsFromPEM(data); !ok { + return nil, fmt.Errorf("unable to load one or more certificates from trust store (file=%s)", c.TrustStoreFile) + } + return trustStore, nil +} diff --git a/network/config_test.go b/network/config_test.go new file mode 100644 index 0000000000..73ee588ddb --- /dev/null +++ b/network/config_test.go @@ -0,0 +1,59 @@ +package network + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDefaultConfig(t *testing.T) { + defs := DefaultConfig() + assert.True(t, defs.EnableTLS) + assert.Equal(t, "network.db", defs.DatabaseFile) + assert.Equal(t, 2000, defs.AdvertHashesInterval) + assert.Equal(t, ":5555", defs.GrpcAddr) +} + +func TestConfig_parseBootstrapNodes(t *testing.T) { + t.Run("empty", func(t *testing.T) { + assert.Empty(t, DefaultConfig().parseBootstrapNodes()) + }) + t.Run("one entry", func(t *testing.T) { + cfg := DefaultConfig() + cfg.BootstrapNodes = "foo:1234" + n := cfg.parseBootstrapNodes() + assert.Len(t, n, 1) + assert.Equal(t, "foo:1234", n[0]) + }) + t.Run("2 entries", func(t *testing.T) { + cfg := DefaultConfig() + cfg.BootstrapNodes = " foo:1234 bar:4321 " + n := cfg.parseBootstrapNodes() + assert.Len(t, n, 2) + assert.Equal(t, "foo:1234", n[0]) + assert.Equal(t, "bar:4321", n[1]) + }) +} + +func TestConfig_loadTrustStore(t *testing.T) { + t.Run("configured", func(t *testing.T) { + cfg := DefaultConfig() + cfg.TrustStoreFile = "test/truststore.pem" + store, err := cfg.loadTrustStore() + assert.NoError(t, err) + assert.NotNil(t, store) + }) + t.Run("invalid PEM file", func(t *testing.T) { + cfg := DefaultConfig() + cfg.TrustStoreFile = "config_test.go" + store, err := cfg.loadTrustStore() + assert.Error(t, err) + assert.Nil(t, store) + }) + t.Run("not configured", func(t *testing.T) { + cfg := DefaultConfig() + cfg.TrustStoreFile = "" + store, err := cfg.loadTrustStore() + assert.Error(t, err) + assert.Nil(t, store) + }) +} diff --git a/network/dag/bboltdag.go b/network/dag/bboltdag.go new file mode 100644 index 0000000000..ecd4500f7c --- /dev/null +++ b/network/dag/bboltdag.go @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "bytes" + "fmt" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto/hash" + log "github.com/nuts-foundation/nuts-node/network/log" + "go.etcd.io/bbolt" +) + +// payloadsBucket is the name of the Bolt bucket that holds the payloads of the documents. +const payloadsBucket = "payloads" + +// documentsBucket is the name of the Bolt bucket that holds the actual documents as JSON. +const documentsBucket = "documents" + +// missingDocumentsBucket is the name of the Bolt bucket that holds the references of the documents we're having prevs +// to, but are missing (and will be added later, hopefully). +const missingDocumentsBucket = "missingdocuments" + +// payloadIndexBucket is the name of the Bolt bucket that holds the a reverse reference from payload hash back to documents. +// The value ([]byte) should be split in chunks of HashSize where each entry is a document reference that refers to +// the payload. +const payloadIndexBucket = "payloadIndex" + +// nextsBucket is the name of the Bolt bucket that holds the forward document references (a.k.a. "nexts") as document +// refs. The value ([]byte) should be split in chunks of HashSize where each entry is a forward reference (next). +const nextsBucket = "nexts" + +// rootDocumentKey is the name of the bucket entry that holds the refs of the root documents. +const rootsDocumentKey = "roots" + +// headsBucket contains the name of the bucket the holds the heads. +const headsBucket = "heads" + +// boltDBFileMode holds the Unix file mode the created BBolt database files will have. +const boltDBFileMode = 0600 + +type bboltDAG struct { + db *bbolt.DB + subscribers map[string]Receiver +} + +type headsStatistic struct { + // SHA256Hash is the last consistency hash. + heads []hash.SHA256Hash +} + +func (d headsStatistic) Name() string { + return "[DAG] Heads" +} + +func (d headsStatistic) String() string { + return fmt.Sprintf("%v", d.heads) +} + +type numberOfDocumentsStatistic struct { + numberOfDocuments int +} + +func (d numberOfDocumentsStatistic) Name() string { + return "[DAG] Number of documents" +} + +func (d numberOfDocumentsStatistic) String() string { + return fmt.Sprintf("%d", d.numberOfDocuments) +} + +type dataSizeStatistic struct { + sizeInBytes int +} + +func (d dataSizeStatistic) Name() string { + return "[DAG] Stored document size (bytes)" +} + +func (d dataSizeStatistic) String() string { + return fmt.Sprintf("%d", d.sizeInBytes) +} + +// NewBBoltDAG creates a etcd/bbolt backed DAG using the given database file path. If the file doesn't exist, it's created. +// The parent directory of the path must exist, otherwise an error could be returned. If the file can't be created or +// read, an error is returned as well. +func NewBBoltDAG(path string) (DAG, PayloadStore, error) { + db, err := bbolt.Open(path, boltDBFileMode, bbolt.DefaultOptions) + if err != nil { + return nil, nil, fmt.Errorf("unable to create bbolt DAG: %w", err) + } + instance := &bboltDAG{ + db: db, + subscribers: map[string]Receiver{}, + } + return instance, instance, nil +} + +func (dag *bboltDAG) Diagnostics() []core.DiagnosticResult { + result := make([]core.DiagnosticResult, 0) + result = append(result, headsStatistic{heads: dag.Heads()}) + documentNum := 0 + _ = dag.db.View(func(tx *bbolt.Tx) error { + if bucket := tx.Bucket([]byte(documentsBucket)); bucket != nil { + documentNum = bucket.Stats().KeyN + } + return nil + }) + result = append(result, numberOfDocumentsStatistic{numberOfDocuments: documentNum}) + // TODO: https://github.com/nuts-foundation/nuts-node/issues/11 + result = append(result, dataSizeStatistic{sizeInBytes: 0}) + return result +} + +func (dag *bboltDAG) Subscribe(documentType string, receiver Receiver) { + oldSubscriber := dag.subscribers[documentType] + dag.subscribers[documentType] = func(document Document, payload []byte) error { + // Chain subscribers in case there's more than 1 + if oldSubscriber != nil { + if err := oldSubscriber(document, payload); err != nil { + return err + } + } + return receiver(document, payload) + } +} + +func (dag bboltDAG) ReadPayload(payloadHash hash.SHA256Hash) ([]byte, error) { + var result []byte + err := dag.db.View(func(tx *bbolt.Tx) error { + if payloads := tx.Bucket([]byte(payloadsBucket)); payloads != nil { + result = payloads.Get(payloadHash.Slice()) + } + return nil + }) + return result, err +} + +func (dag bboltDAG) WritePayload(payloadHash hash.SHA256Hash, data []byte) error { + err := dag.db.Update(func(tx *bbolt.Tx) error { + payloads, err := tx.CreateBucketIfNotExists([]byte(payloadsBucket)) + if err != nil { + return err + } + if err := payloads.Put(payloadHash.Slice(), data); err != nil { + return err + } + return nil + }) + if err == nil { + dag.payloadReceived(payloadHash, data) + } + return err +} + +func (dag bboltDAG) Get(ref hash.SHA256Hash) (Document, error) { + var result Document + var err error + err = dag.db.View(func(tx *bbolt.Tx) error { + if documents := tx.Bucket([]byte(documentsBucket)); documents != nil { + result, err = getDocument(ref, documents) + return err + } + return nil + }) + return result, err +} + +func (dag bboltDAG) GetByPayloadHash(payloadHash hash.SHA256Hash) ([]Document, error) { + result := make([]Document, 0) + err := dag.db.View(func(tx *bbolt.Tx) error { + documents := tx.Bucket([]byte(documentsBucket)) + payloadIndex := tx.Bucket([]byte(payloadIndexBucket)) + if documents == nil || payloadIndex == nil { + return nil + } + documentHashes := parseHashList(payloadIndex.Get(payloadHash.Slice())) + for _, documentHash := range documentHashes { + document, err := getDocument(documentHash, documents) + if err != nil { + return err + } + result = append(result, document) + } + return nil + }) + return result, err +} + +func (dag bboltDAG) Heads() []hash.SHA256Hash { + result := make([]hash.SHA256Hash, 0) + _ = dag.db.View(func(tx *bbolt.Tx) error { + heads := tx.Bucket([]byte(headsBucket)) + if heads == nil { + return nil + } + cursor := heads.Cursor() + for ref, _ := cursor.First(); ref != nil; ref, _ = cursor.Next() { + result = append(result, hash.FromSlice(ref)) + } + return nil + }) + return result +} + +func (dag bboltDAG) All() ([]Document, error) { + result := make([]Document, 0) + err := dag.db.View(func(tx *bbolt.Tx) error { + if documents := tx.Bucket([]byte(documentsBucket)); documents != nil { + cursor := documents.Cursor() + for ref, documentBytes := cursor.First(); documentBytes != nil; ref, documentBytes = cursor.Next() { + if bytes.Equal(ref, []byte(rootsDocumentKey)) { + continue + } + document, err := ParseDocument(documentBytes) + if err != nil { + return fmt.Errorf("unable to parse document %s: %w", ref, err) + } + result = append(result, document) + } + return nil + } + return nil + }) + return result, err +} + +func (dag bboltDAG) IsPresent(ref hash.SHA256Hash) (bool, error) { + return dag.isPresent(documentsBucket, ref.Slice()) +} + +func (dag bboltDAG) IsPayloadPresent(payloadHash hash.SHA256Hash) (bool, error) { + return dag.isPresent(payloadsBucket, payloadHash.Slice()) +} + +func (dag bboltDAG) MissingDocuments() []hash.SHA256Hash { + result := make([]hash.SHA256Hash, 0) + if err := dag.db.View(func(tx *bbolt.Tx) error { + if bucket := tx.Bucket([]byte(missingDocumentsBucket)); bucket != nil { + cursor := bucket.Cursor() + for ref, _ := cursor.First(); ref != nil; ref, _ = cursor.Next() { + result = append(result, hash.FromSlice(ref)) + } + } + return nil + }); err != nil { + log.Logger().Errorf("Unable to fetch missing documents: %v", err) + } + return result +} + +func (dag *bboltDAG) Add(documents ...Document) error { + for _, document := range documents { + if document != nil { + if err := dag.add(document); err != nil { + return err + } + } + } + return nil +} + +func (dag bboltDAG) Walk(walker Walker, visitor Visitor, startAt hash.SHA256Hash) error { + return dag.db.View(func(tx *bbolt.Tx) error { + documents := tx.Bucket([]byte(documentsBucket)) + nexts := tx.Bucket([]byte(nextsBucket)) + if documents == nil || nexts == nil { + // DAG is empty + return nil + } + return walker.walk(visitor, startAt, func(hash hash.SHA256Hash) (Document, error) { + return getDocument(hash, documents) + }, func(hash hash.SHA256Hash) ([]hash.SHA256Hash, error) { + return parseHashList(nexts.Get(hash.Slice())), nil + }, documents.Stats().KeyN) // TODO Optimization: should we cache this number of keys? + }) +} + +func (dag bboltDAG) Root() (hash hash.SHA256Hash, err error) { + err = dag.db.View(func(tx *bbolt.Tx) error { + if documents := tx.Bucket([]byte(documentsBucket)); documents != nil { + if roots := getRoots(documents); len(roots) >= 1 { + hash = roots[0] + } + } + return nil + }) + return +} + +func (dag bboltDAG) isPresent(bucketName string, key []byte) (bool, error) { + var result bool + var err error + err = dag.db.View(func(tx *bbolt.Tx) error { + if payloads := tx.Bucket([]byte(bucketName)); payloads != nil { + data := payloads.Get(key) + result = len(data) > 0 + } + return nil + }) + return result, err +} + +func (dag *bboltDAG) add(document Document) error { + ref := document.Ref() + refSlice := ref.Slice() + return dag.db.Update(func(tx *bbolt.Tx) error { + documents, nexts, missingDocuments, payloadIndex, _, heads, err := getBuckets(tx) + if err != nil { + return err + } + if exists(documents, ref) { + log.Logger().Tracef("Document %s already exists, not adding it again.", ref) + return nil + } + if len(document.Previous()) == 0 { + if getRoots(documents) != nil { + return errRootAlreadyExists + } + if err := addRoot(documents, ref); err != nil { + return fmt.Errorf("unable to register root %s: %w", ref, err) + } + } + if err := documents.Put(refSlice, document.Data()); err != nil { + return err + } + // Store forward references ([C -> prev A, B] is stored as [A -> C, B -> C]) + for _, prev := range document.Previous() { + if err := dag.registerNextRef(nexts, prev, ref); err != nil { + return fmt.Errorf("unable to store forward reference %s->%s: %w", prev, ref, err) + } + if !exists(documents, prev) { + log.Logger().Debugf("Document %s is referring to missing prev %s, marking it as missing", ref, prev) + if err = missingDocuments.Put(prev.Slice(), []byte{1}); err != nil { + return fmt.Errorf("unable to register missing document %s: %w", prev, err) + } + } + if err := heads.Delete(prev.Slice()); err != nil { + return fmt.Errorf("unable to remove earlier head: %w", err) + } + } + // See if this is a head + if len(missingDocuments.Get(refSlice)) == 0 { + // This is not a previously missing document, so it is a head (for now) + if err := heads.Put(refSlice, []byte{1}); err != nil { + return fmt.Errorf("unable to mark document as head (ref=%s): %w", ref, err) + } + } + // Store reverse reference from payload hash to document + newPayloadIndexValue := appendHashList(payloadIndex.Get(document.Payload().Slice()), ref) + if err = payloadIndex.Put(document.Payload().Slice(), newPayloadIndexValue); err != nil { + return fmt.Errorf("unable to update payload index for document %s: %w", ref, err) + } + // Remove marker if this document was previously missing + return missingDocuments.Delete(refSlice) + }) +} + +func getBuckets(tx *bbolt.Tx) (documents, nexts, missingDocuments, payloadIndex, payloads, heads *bbolt.Bucket, err error) { + if documents, err = tx.CreateBucketIfNotExists([]byte(documentsBucket)); err != nil { + return + } + if nexts, err = tx.CreateBucketIfNotExists([]byte(nextsBucket)); err != nil { + return + } + if missingDocuments, err = tx.CreateBucketIfNotExists([]byte(missingDocumentsBucket)); err != nil { + return + } + if payloadIndex, err = tx.CreateBucketIfNotExists([]byte(payloadIndexBucket)); err != nil { + return + } + if payloads, err = tx.CreateBucketIfNotExists([]byte(payloadsBucket)); err != nil { + return + } + if heads, err = tx.CreateBucketIfNotExists([]byte(headsBucket)); err != nil { + return + } + return +} + +func getRoots(documentsBucket *bbolt.Bucket) []hash.SHA256Hash { + return parseHashList(documentsBucket.Get([]byte(rootsDocumentKey))) +} + +func addRoot(documentsBucket *bbolt.Bucket, ref hash.SHA256Hash) error { + roots := appendHashList(documentsBucket.Get([]byte(rootsDocumentKey)), ref) + return documentsBucket.Put([]byte(rootsDocumentKey), roots) +} + +// registerNextRef registers a forward reference a.k.a. "next", in contrary to "prev(s)" which is the inverse of the relation. +// It takes the nexts bucket, the prev and the next. Given document A and B where B prevs A, prev = A, next = B. +func (dag *bboltDAG) registerNextRef(nextsBucket *bbolt.Bucket, prev hash.SHA256Hash, next hash.SHA256Hash) error { + prevSlice := prev.Slice() + value := nextsBucket.Get(prevSlice) + if value == nil { + // No entry yet for this prev + return nextsBucket.Put(prevSlice, next.Slice()) + } + // Existing entry for this prev so add this one to it + return nextsBucket.Put(prevSlice, appendHashList(value, next)) +} + +func (dag *bboltDAG) payloadReceived(payloadHash hash.SHA256Hash, payload []byte) { + // TODO: This is a stupid implementation that doesn't retry failed subscribers or publish documents in-order + // (since documents and payload may arrive out-of-order). Should be changed to something more intelligent. + err := dag.db.View(func(tx *bbolt.Tx) error { + documents := tx.Bucket([]byte(documentsBucket)) + payloadsIndex := tx.Bucket([]byte(payloadIndexBucket)) + if documents == nil || payloadsIndex == nil { + return nil + } + for _, documentRef := range parseHashList(payloadsIndex.Get(payloadHash.Slice())) { + if document, err := getDocument(documentRef, documents); err != nil { + return err + } else if receiver := dag.subscribers[document.PayloadType()]; receiver == nil { + continue + } else if err := receiver(document, payload); err != nil { + // TODO: Should this be done in a goroutine to make sure applications don't block network processes? + log.Logger().Errorf("Document subscriber returned an error (document=%s,type=%s): %v", document.Ref(), document.PayloadType(), err) + } + } + return nil + }) + if err != nil { + log.Logger().Errorf("Unable to publish document (payload=%s): %v", payloadHash, err) + } +} + +func getDocument(hash hash.SHA256Hash, documents *bbolt.Bucket) (Document, error) { + documentBytes := documents.Get(hash.Slice()) + if documentBytes == nil { + return nil, nil + } + document, err := ParseDocument(documentBytes) + if err != nil { + return nil, fmt.Errorf("unable to parse document %s: %w", hash, err) + } + return document, nil +} + +// exists checks whether the document with the given ref exists. +func exists(documents *bbolt.Bucket, ref hash.SHA256Hash) bool { + return documents.Get(ref.Slice()) != nil +} + +// parseHashList splits a list of concatenated hashes into separate hashes. +func parseHashList(input []byte) []hash.SHA256Hash { + if len(input) == 0 { + return nil + } + num := (len(input) - (len(input) % hash.SHA256HashSize)) / hash.SHA256HashSize + result := make([]hash.SHA256Hash, num) + for i := 0; i < num; i++ { + result[i] = hash.FromSlice(input[i*hash.SHA256HashSize : i*hash.SHA256HashSize+hash.SHA256HashSize]) + } + return result +} + +func appendHashList(list []byte, h hash.SHA256Hash) []byte { + newList := make([]byte, 0, len(list)+hash.SHA256HashSize) + newList = append(newList, list...) + newList = append(newList, h.Slice()...) + return newList +} diff --git a/network/dag/bboltdag_test.go b/network/dag/bboltdag_test.go new file mode 100644 index 0000000000..2ea6f35668 --- /dev/null +++ b/network/dag/bboltdag_test.go @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/test/io" + "github.com/stretchr/testify/assert" + "path" + "sort" + "strings" + "testing" +) + +var bboltDAGCreator = func(t *testing.T) DAG { + if dag, _, err := NewBBoltDAG(path.Join(io.TestDirectory(t), "dag.db")); err != nil { + t.Fatal(err) + return nil + } else { + return dag + } +} + +func TestBBoltDAG_Add(t *testing.T) { + DAGTest_Add(bboltDAGCreator, t) +} + +func TestBBoltDAG_All(t *testing.T) { + DAGTest_All(bboltDAGCreator, t) +} + +func TestBBoltDAG_MissingDocuments(t *testing.T) { + DAGTest_MissingDocuments(bboltDAGCreator, t) +} + +func TestBBoltDAG_Walk(t *testing.T) { + DAGTest_Walk(bboltDAGCreator, t) +} + +func TestBBoltDAG_Get(t *testing.T) { + DAGTest_Get(bboltDAGCreator, t) +} + +func TestBBoltDAG_GetByPayloadHash(t *testing.T) { + DAGTest_GetByPayloadHash(bboltDAGCreator, t) +} + +func TestBBoltDAG_PayloadStore(t *testing.T) { + PayloadStoreTest(func(t *testing.T) PayloadStore { + return bboltDAGCreator(t).(PayloadStore) + }, t) +} + +func TestBBoltDAG_Subscribe(t *testing.T) { + DAGTest_Subscribe(bboltDAGCreator, t) +} + +func TestBBoltDAG_Diagnostics(t *testing.T) { + dag := bboltDAGCreator(t).(*bboltDAG) + doc1 := CreateTestDocument(2) + dag.Add(doc1) + diagnostics := dag.Diagnostics() + assert.Len(t, diagnostics, 3) + // Assert actual diagnostics + lines := make([]string, 0) + for _, diagnostic := range diagnostics { + lines = append(lines, diagnostic.Name()+": "+diagnostic.String()) + } + sort.Strings(lines) + actual := strings.Join(lines, "\n") + assert.Equal(t, `[DAG] Heads: [`+doc1.Ref().String()+`] +[DAG] Number of documents: 2 +[DAG] Stored document size (bytes): 0`, actual) +} + +func Test_parseHashList(t *testing.T) { + t.Run("empty", func(t *testing.T) { + assert.Empty(t, parseHashList([]byte{})) + }) + t.Run("1 entry", func(t *testing.T) { + h1 := hash.SHA256Sum([]byte("Hello, World!")) + actual := parseHashList(h1[:]) + assert.Len(t, actual, 1) + assert.Equal(t, hash.FromSlice(h1[:]), actual[0]) + }) + t.Run("2 entries", func(t *testing.T) { + h1 := hash.SHA256Sum([]byte("Hello, World!")) + h2 := hash.SHA256Sum([]byte("Hello, All!")) + actual := parseHashList(append(h1[:], h2[:]...)) + assert.Len(t, actual, 2) + assert.Equal(t, hash.FromSlice(h1[:]), actual[0]) + assert.Equal(t, hash.FromSlice(h2[:]), actual[1]) + }) + t.Run("2 entries, dangling bytes", func(t *testing.T) { + h1 := hash.SHA256Sum([]byte("Hello, World!")) + h2 := hash.SHA256Sum([]byte("Hello, All!")) + input := append(h1[:], h2[:]...) + input = append(input, 1, 2, 3) // Add some dangling bytes + actual := parseHashList(input) + assert.Len(t, actual, 2) + assert.Equal(t, hash.FromSlice(h1[:]), actual[0]) + assert.Equal(t, hash.FromSlice(h2[:]), actual[1]) + }) +} diff --git a/network/dag/dag.go b/network/dag/dag.go new file mode 100644 index 0000000000..ca0cc4f71a --- /dev/null +++ b/network/dag/dag.go @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "container/list" + "errors" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "sort" +) + +var errRootAlreadyExists = errors.New("root document already exists") + +// DAG is a directed acyclic graph consisting of nodes (documents) referring to preceding nodes. +type DAG interface { + Publisher + // Add adds one or more documents to the DAG. If it can't be added an error is returned. Nil entries are ignored. + Add(documents ...Document) error + // MissingDocuments returns the hashes of the documents we know we are missing and should still be resolved. + MissingDocuments() []hash.SHA256Hash + // Walk visits every node of the DAG, starting at the given hash working its way down each level until every leaf is visited. + // when startAt is an empty hash, the walker starts at the root node. + Walk(walker Walker, visitor Visitor, startAt hash.SHA256Hash) error + // Root returns the root hash of the DAG. If there's no root an empty hash is returned. If an error occurs, it is returned. + Root() (hash.SHA256Hash, error) + // Get retrieves a specific document from the DAG. If it isn't found, nil is returned. + Get(ref hash.SHA256Hash) (Document, error) + // GetByPayloadHash retrieves all documents that refer to the specified payload. + GetByPayloadHash(payloadHash hash.SHA256Hash) ([]Document, error) + // All retrieves all documents from the DAG. + // TODO: This should go when there's a more optimized network protocol + All() ([]Document, error) + // IsPresent checks whether the specified document exists on the DAG. + IsPresent(ref hash.SHA256Hash) (bool, error) + // Heads returns all unmerged heads, which are documents where no other documents point to as `prev`. To be used + // as `prevs` parameter when adding a new document. + Heads() []hash.SHA256Hash +} + +// Publisher defines the interface for types that publish Nuts Network documents. +type Publisher interface { + // Subscribe lets an application subscribe to a specific type of document. When a new document is received (for the + // first time) the `receiver` function is called. + Subscribe(documentType string, receiver Receiver) +} + +// Receiver defines a function for processing documents when walking the DAG. +type Receiver func(document Document, payload []byte) error + +// Walker defines the interface for a type that can walk the DAG. +type Walker interface { + // walk visits every node of the DAG, starting at the given start node and working down each level until every leaf is visited. + // numberOfNodes is an indicative number of nodes that's expected to be visited. It's used for optimizing memory usage. + // getFn is a function for reading a document from the DAG using the given ref hash. If not found nil must be returned. + // nextsFn is a function for reading a document's nexts using the given ref hash. If not found nil must be returned. + walk(visitor Visitor, startAt hash.SHA256Hash, getFn func(hash.SHA256Hash) (Document, error), nextsFn func(hash.SHA256Hash) ([]hash.SHA256Hash, error), numberOfNodes int) error +} + +// BFSWalker walks the DAG using the Breadth-First-Search (BFS) as described by Anany Levitin in "The Design & Analysis of Algorithms". +// It visits the whole tree level for level (breadth first vs depth first). It works by taking a node from queue and +// then adds the node's children (downward edges) to the queue. It starts by adding the root node to the queue and +// loops over the queue until empty, meaning all nodes reachable from the root node have been visited. Since our +// DAG contains merges (two parents referring to the same child node) we also keep a map to avoid visiting a +// merger node twice. +// +// This also means we have to make sure we don't visit the merger node before all of its previous nodes have been +// visited, which BFS doesn't account for. If that happens we skip the node without marking it as visited, +// so it will be visited again when the unvisited previous node is visited, which re-adds the merger node to the queue. +type BFSWalker struct{} + +func (w BFSWalker) walk(visitor Visitor, startAt hash.SHA256Hash, getFn func(hash.SHA256Hash) (Document, error), nextsFn func(hash.SHA256Hash) ([]hash.SHA256Hash, error), numberOfNodes int) error { + queue := list.New() + queue.PushBack(startAt) + visitedDocuments := make(map[hash.SHA256Hash]bool, numberOfNodes) + +ProcessQueueLoop: + for queue.Len() > 0 { + // Pop first element of queue + front := queue.Front() + queue.Remove(front) + currentRef := front.Value.(hash.SHA256Hash) + + // Make sure we haven't already visited this node + if _, visited := visitedDocuments[currentRef]; visited { + continue + } + + // Make sure all prevs have been visited. Otherwise just continue, it will be re-added to the queue when the + // unvisited prev node is visited and re-adds this node to the processing queue. + currentDocument, err := getFn(currentRef) + if err != nil { + return err + } + + for _, prev := range currentDocument.Previous() { + if _, visited := visitedDocuments[prev]; !visited { + continue ProcessQueueLoop + } + } + + // Add child nodes to processing queue + // Processing order of nodes on the same level doesn't really matter for correctness of the DAG travel + // but it makes testing easier. + if nexts, err := nextsFn(currentRef); err != nil { + return err + } else if nexts != nil { + sortedEdges := make([]hash.SHA256Hash, 0, len(nexts)) + for _, nextNode := range nexts { + sortedEdges = append(sortedEdges, nextNode) + } + sort.Slice(sortedEdges, func(i, j int) bool { + return sortedEdges[i].Compare(sortedEdges[j]) < 0 + }) + for _, nextNode := range sortedEdges { + queue.PushBack(nextNode) + } + } + + // Visit the node + if !visitor(currentDocument) { + break + } + + // Mark this node as visited + visitedDocuments[currentRef] = true + } + return nil +} + +// Visitor defines the contract for a function that visits the DAG. If the function returns `false` it stops walking the DAG. +type Visitor func(document Document) bool + +// PayloadStore defines the interface for types that store document payloads. +type PayloadStore interface { + + // IsPayloadPresent checks whether the contents for the given document are present. + IsPayloadPresent(payloadHash hash.SHA256Hash) (bool, error) + + // ReadPayload reads the contents for the specified payload, identified by the given hash. If contents can't be found, + // nil is returned. If something (else) goes wrong an error is returned. + ReadPayload(payloadHash hash.SHA256Hash) ([]byte, error) + + // WritePayload writes contents for the specified payload, identified by the given hash. Implementations must make + // sure the hash matches the given contents. + WritePayload(payloadHash hash.SHA256Hash, data []byte) error +} diff --git a/network/dag/dag_contract_test.go b/network/dag/dag_contract_test.go new file mode 100644 index 0000000000..783dcb2fc4 --- /dev/null +++ b/network/dag/dag_contract_test.go @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "errors" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +// trackingVisitor just keeps track of which nodes were visited in what order. +type trackingVisitor struct { + documents []Document +} + +func (n *trackingVisitor) Accept(document Document) bool { + n.documents = append(n.documents, document) + return true +} + +func (n trackingVisitor) JoinRefsAsString() string { + var contents []string + for _, document := range n.documents { + val := strings.TrimLeft(document.Payload().String(), "0") + if val == "" { + val = "0" + } + contents = append(contents, val) + } + return strings.Join(contents, ", ") +} + +func DAGTest_All(creator func(t *testing.T) DAG, t *testing.T) { + t.Run("ok", func(t *testing.T) { + graph := creator(t) + doc := CreateTestDocument(1) + + err := graph.Add(doc) + + if !assert.NoError(t, err) { + return + } + + actual, err := graph.All() + if !assert.NoError(t, err) { + return + } + assert.Len(t, actual, 1) + assert.Equal(t, doc, actual[0]) + }) +} + +func DAGTest_Get(creator func(t *testing.T) DAG, t *testing.T) { + t.Run("found", func(t *testing.T) { + graph := creator(t) + document := CreateTestDocument(1) + _ = graph.Add(document) + actual, err := graph.Get(document.Ref()) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, document, actual) + }) + t.Run("not found", func(t *testing.T) { + graph := creator(t) + actual, err := graph.Get(hash.SHA256Sum([]byte{1, 2, 3})) + assert.NoError(t, err) + assert.Nil(t, actual) + }) +} + +func DAGTest_Add(creator func(t *testing.T) DAG, t *testing.T) { + t.Run("ok", func(t *testing.T) { + graph := creator(t) + doc := CreateTestDocument(0) + + err := graph.Add(doc) + + assert.NoError(t, err) + visitor := trackingVisitor{} + root, _ := graph.Root() + err = graph.Walk(&BFSWalker{}, visitor.Accept, root) + if !assert.NoError(t, err) { + return + } + assert.Len(t, visitor.documents, 1) + assert.Equal(t, doc.Ref(), visitor.documents[0].Ref()) + present, _ := graph.IsPresent(doc.Ref()) + assert.True(t, present) + }) + t.Run("duplicate", func(t *testing.T) { + graph := creator(t) + doc := CreateTestDocument(0) + + _ = graph.Add(doc) + err := graph.Add(doc) + assert.NoError(t, err) + actual, _ := graph.All() + assert.Len(t, actual, 1) + }) + t.Run("second root", func(t *testing.T) { + graph := creator(t) + root1 := CreateTestDocument(1) + root2 := CreateTestDocument(2) + + _ = graph.Add(root1) + err := graph.Add(root2) + assert.EqualError(t, err, "root document already exists") + actual, _ := graph.All() + assert.Len(t, actual, 1) + }) + t.Run("ok - out of order", func(t *testing.T) { + _, documents := graphF(creator, t) + graph := creator(t) + + for i := len(documents) - 1; i >= 0; i-- { + err := graph.Add(documents[i]) + if !assert.NoError(t, err) { + return + } + } + + visitor := trackingVisitor{} + root, _ := graph.Root() + err := graph.Walk(&BFSWalker{}, visitor.Accept, root) + if !assert.NoError(t, err) { + return + } + assert.Regexp(t, "0, (1, 2|2, 1), (3, 4|4, 3), 5", visitor.JoinRefsAsString()) + }) + t.Run("error - cyclic graph", func(t *testing.T) { + t.Skip("Algorithm for detecting cycles is not yet decided on") + // A -> B -> C -> B + A := CreateTestDocument(0) + B := CreateTestDocument(1, A.Ref()).(*document) + C := CreateTestDocument(2, B.Ref()) + B.prevs = append(B.prevs, C.Ref()) + + graph := creator(t) + err := graph.Add(A, B, C) + assert.EqualError(t, err, "") + }) +} + +func DAGTest_Walk(creator func(t *testing.T) DAG, t *testing.T) { + t.Run("ok - empty graph", func(t *testing.T) { + graph := creator(t) + visitor := trackingVisitor{} + + root, _ := graph.Root() + err := graph.Walk(&BFSWalker{}, visitor.Accept, root) + if !assert.NoError(t, err) { + return + } + + assert.Empty(t, visitor.documents) + }) +} + +func DAGTest_MissingDocuments(creator func(t *testing.T) DAG, t *testing.T) { + A := CreateTestDocument(0) + B := CreateTestDocument(1, A.Ref()) + C := CreateTestDocument(2, B.Ref()) + t.Run("no missing documents (empty graph)", func(t *testing.T) { + graph := creator(t) + assert.Empty(t, graph.MissingDocuments()) + }) + t.Run("no missing documents (non-empty graph)", func(t *testing.T) { + graph := creator(t) + graph.Add(A, B, C) + assert.Empty(t, graph.MissingDocuments()) + }) + t.Run("missing documents (non-empty graph)", func(t *testing.T) { + graph := creator(t) + graph.Add(A, C) + assert.Len(t, graph.MissingDocuments(), 1) + // Now add missing document B and assert there are no more missing documents + graph.Add(B) + assert.Empty(t, graph.MissingDocuments()) + }) +} + +func DAGTest_Subscribe(creator func(t *testing.T) DAG, t *testing.T) { + t.Run("no subscribers", func(t *testing.T) { + graph := creator(t) + document := CreateTestDocument(1) + _ = graph.Add(document) + err := graph.(PayloadStore).WritePayload(document.Payload(), []byte("foobar")) + assert.NoError(t, err) + }) + t.Run("single subscriber", func(t *testing.T) { + graph := creator(t) + document := CreateTestDocument(1) + received := false + graph.Subscribe(document.PayloadType(), func(actualDocument Document, actualPayload []byte) error { + assert.Equal(t, document, actualDocument) + assert.Equal(t, []byte("foobar"), actualPayload) + received = true + return nil + }) + _ = graph.Add(document) + err := graph.(PayloadStore).WritePayload(document.Payload(), []byte("foobar")) + assert.NoError(t, err) + assert.True(t, received) + }) + t.Run("multiple subscribers", func(t *testing.T) { + graph := creator(t) + document := CreateTestDocument(1) + calls := 0 + receiver := func(actualDocument Document, actualPayload []byte) error { + calls++ + return nil + } + graph.Subscribe(document.PayloadType(), receiver) + graph.Subscribe(document.PayloadType(), receiver) + _ = graph.Add(document) + err := graph.(PayloadStore).WritePayload(document.Payload(), []byte("foobar")) + assert.NoError(t, err) + assert.Equal(t, 2, calls) + }) + t.Run("multiple subscribers, first fails", func(t *testing.T) { + graph := creator(t) + document := CreateTestDocument(1) + calls := 0 + receiver := func(actualDocument Document, actualPayload []byte) error { + calls++ + return errors.New("failed") + } + graph.Subscribe(document.PayloadType(), receiver) + graph.Subscribe(document.PayloadType(), receiver) + _ = graph.Add(document) + err := graph.(PayloadStore).WritePayload(document.Payload(), []byte("foobar")) + assert.NoError(t, err) + assert.Equal(t, 1, calls) + }) + t.Run("subscriber error", func(t *testing.T) { + graph := creator(t) + document := CreateTestDocument(1) + graph.Subscribe(document.PayloadType(), func(actualDocument Document, actualPayload []byte) error { + return errors.New("failed") + }) + _ = graph.Add(document) + err := graph.(PayloadStore).WritePayload(document.Payload(), []byte("foobar")) + assert.NoError(t, err) + }) +} + +func DAGTest_GetByPayloadHash(creator func(t *testing.T) DAG, t *testing.T) { + t.Run("found", func(t *testing.T) { + graph := creator(t) + document := CreateTestDocument(1) + _ = graph.Add(document) + actual, err := graph.GetByPayloadHash(document.Payload()) + assert.Len(t, actual, 1) + assert.NoError(t, err) + assert.Equal(t, document, actual[0]) + }) + t.Run("not found", func(t *testing.T) { + graph := creator(t) + actual, err := graph.GetByPayloadHash(hash.SHA256Sum([]byte{1, 2, 3})) + assert.NoError(t, err) + assert.Empty(t, actual) + }) +} + +func PayloadStoreTest(creator func(t *testing.T) PayloadStore, t *testing.T) { + t.Run("roundtrip", func(t *testing.T) { + payload := []byte("Hello, World!") + hash := hash.SHA256Sum(payload) + payloadStore := creator(t) + // Before, payload should not be present + present, err := payloadStore.IsPayloadPresent(hash) + if !assert.NoError(t, err) || !assert.False(t, present) { + return + } + // Add payload + err = payloadStore.WritePayload(hash, payload) + if !assert.NoError(t, err) { + return + } + // Now it should be present + present, err = payloadStore.IsPayloadPresent(hash) + if !assert.NoError(t, err) || !assert.True(t, present, "payload should be present") { + return + } + // Read payload + data, err := payloadStore.ReadPayload(hash) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, payload, data) + }) +} + +// graphF creates the following graph: +//..................A +//................/ \ +//...............B C +//...............\ / \ +//.................D E +//.......................\ +//........................F +func graphF(creator func(t *testing.T) DAG, t *testing.T) (DAG, []Document) { + graph := creator(t) + A := CreateTestDocument(0) + B := CreateTestDocument(1, A.Ref()) + C := CreateTestDocument(2, A.Ref()) + D := CreateTestDocument(3, B.Ref(), C.Ref()) + E := CreateTestDocument(4, C.Ref()) + F := CreateTestDocument(5, E.Ref()) + docs := []Document{A, B, C, D, E, F} + graph.Add(docs...) + return graph, docs +} diff --git a/network/dag/dag_test.go b/network/dag/dag_test.go new file mode 100644 index 0000000000..2feb1072e8 --- /dev/null +++ b/network/dag/dag_test.go @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBFSWalker(t *testing.T) { + t.Run("ok - walk graph F", func(t *testing.T) { + visitor := trackingVisitor{} + graph, _ := graphF(bboltDAGCreator, t) + + root, _ := graph.Root() + err := graph.Walk(&BFSWalker{}, visitor.Accept, root) + if !assert.NoError(t, err) { + return + } + + assert.Regexp(t, "0, (1, 2|2, 1), (3, 4|4, 3), 5", visitor.JoinRefsAsString()) + }) + + t.Run("ok - walk graph G", func(t *testing.T) { + //..................A + //................/ \ + //...............B C + //...............\ / \ + //.................D E + //.................\.....\ + //..................\.....F + //...................\.../ + //.....................G + visitor := trackingVisitor{} + graph, docs := graphF(bboltDAGCreator, t) + G := CreateTestDocument(6, docs[3].Ref(), docs[5].Ref()) + graph.Add(G) + + root, _ := graph.Root() + graph.Walk(&BFSWalker{}, visitor.Accept, root) + + assert.Regexp(t, "0, (1, 2|2, 1), (3, 4|4, 3), 5, 6", visitor.JoinRefsAsString()) + }) + + t.Run("ok - walk graph F, C is missing", func(t *testing.T) { + //..................A + //................/ \ + //...............B C (missing) + //...............\ / \ + //.................D E + //.......................\ + //........................F + visitor := trackingVisitor{} + _, docs := graphF(bboltDAGCreator, t) + graph := bboltDAGCreator(t) + graph.Add(docs[0], docs[1], docs[3], docs[4], docs[5]) + + root, _ := graph.Root() + graph.Walk(&BFSWalker{}, visitor.Accept, root) + + assert.Equal(t, "0, 1", visitor.JoinRefsAsString()) + }) + + t.Run("ok - empty graph", func(t *testing.T) { + graph := bboltDAGCreator(t) + visitor := trackingVisitor{} + + root, _ := graph.Root() + err := graph.Walk(&BFSWalker{}, visitor.Accept, root) + if !assert.NoError(t, err) { + return + } + + assert.Empty(t, visitor.documents) + }) + + t.Run("ok - document added twice", func(t *testing.T) { + graph := bboltDAGCreator(t) + d := CreateTestDocument(0) + graph.Add(d) + graph.Add(d) + visitor := trackingVisitor{} + + root, _ := graph.Root() + graph.Walk(&BFSWalker{}, visitor.Accept, root) + + assert.Len(t, visitor.documents, 1) + }) + + t.Run("error - second root document", func(t *testing.T) { + graph := bboltDAGCreator(t) + d1 := CreateTestDocument(0) + d2 := CreateTestDocument(1) + err := graph.Add(d1) + + err = graph.Add(d2) + assert.Equal(t, errRootAlreadyExists, err) + visitor := trackingVisitor{} + + root, _ := graph.Root() + graph.Walk(&BFSWalker{}, visitor.Accept, root) + + assert.Len(t, visitor.documents, 1) + assert.Equal(t, d1.Data(), visitor.documents[0].Data()) + }) +} diff --git a/network/dag/document.go b/network/dag/document.go new file mode 100644 index 0000000000..9f9e9cd57f --- /dev/null +++ b/network/dag/document.go @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "crypto" + "encoding/json" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/pkg/errors" + "strings" + "time" +) + +// Version defines a type for distributed document format version. +type Version int + +const currentVersion = 1 +const signingTimeHeader = "sigt" +const versionHeader = "ver" +const previousHeader = "prevs" +const timelineIDHeader = "tid" +const timelineVersionHeader = "tiv" + +var allowedAlgos = []jwa.SignatureAlgorithm{jwa.ES256, jwa.ES384, jwa.ES512, jwa.PS256, jwa.PS384, jwa.PS512} + +var errInvalidPayloadType = errors.New("payload type must be formatted as MIME type") +var errInvalidPrevs = errors.New("prevs contains an empty hash") +var unableToParseDocumentErrFmt = "unable to parse document: %w" +var documentNotValidErrFmt = "document validation failed: %w" +var missingHeaderErrFmt = "missing %s header" +var invalidHeaderErrFmt = "invalid %s header" + +// UnsignedDocument holds the base properties of a document which can be signed to create a Document. +type UnsignedDocument interface { + // PayloadType returns the MIME-formatted type of the payload. It must contain the context and specific type of the + // payload, e.g. 'registry/endpoint'. + PayloadType() string + // Payload returns the hash of the payload of the document. + Payload() hash.SHA256Hash + // Previous returns the references of the previous documents this document points to. + Previous() []hash.SHA256Hash + // Version returns the version number of the distributed document format. + Version() Version + // TimelineID returns the timeline ID of the document. + TimelineID() hash.SHA256Hash + // TimelineVersion returns the timeline version of the document. If the returned version is < 1 the timeline version + // is not set. + TimelineVersion() int +} + +// Document defines a signed distributed document as described by RFC004 - Distributed Document Format. +type Document interface { + UnsignedDocument + // SigningKey returns the key that was used to sign the document as JWK. If this field is not set SigningKeyID + // must be used to resolve the signing key. + SigningKey() jwk.Key + // SigningKeyID returns the ID of the key that was used to sign the document. It can be used to look up the key. + SigningKeyID() string + // SigningTime returns the time that the document was signed. + SigningTime() time.Time + // Ref returns the reference to this document. + Ref() hash.SHA256Hash + // Data returns the byte representation of this document which can be used for transport. + Data() []byte + // VerifySignature verifies that the signature is correct. A function has to be supplied to look up the signing key + // using the signing key ID, when the signing key is specified as `kid` header than than being included as `jwk`. + // An error is returned if verification fails or something else goes wrong. + VerifySignature(func(string) crypto.PublicKey) error + json.Marshaler +} + +// NewDocument creates a new unsigned document. Parameters payload and payloadType can't be empty, but prevs is optional. +// Prevs must not contain empty or invalid hashes. +func NewDocument(payload hash.SHA256Hash, payloadType string, prevs []hash.SHA256Hash, additionalFields ...FieldOpt) (UnsignedDocument, error) { + if !ValidatePayloadType(payloadType) { + return nil, errInvalidPayloadType + } + for _, prev := range prevs { + if prev.Empty() { + return nil, errInvalidPrevs + } + } + result := document{ + payload: payload, + payloadType: payloadType, + version: currentVersion, + } + if len(prevs) > 0 { + result.prevs = append(prevs) + } + for _, field := range additionalFields { + field(&result) + } + return &result, nil +} + +// FieldOpt defines a function for specifying fields on a document. +type FieldOpt func(target *document) + +// TimelineVersionField adds the timeline version field to a document. +func TimelineVersionField(version int) FieldOpt { + return func(target *document) { + target.timelineVersion = version + } +} + +// ValidatePayloadType checks whether the payload type is valid according to RFC004. +func ValidatePayloadType(payloadType string) bool { + return strings.Contains(payloadType, "/") +} + +type document struct { + prevs []hash.SHA256Hash + payload hash.SHA256Hash + payloadType string + signingKey jwk.Key + signingKeyID string + signingTime time.Time + version Version + timelineID hash.SHA256Hash + timelineVersion int + + data []byte + ref hash.SHA256Hash +} + +func (d document) MarshalJSON() ([]byte, error) { + return json.Marshal(string(d.Data())) +} + +func (d document) Data() []byte { + return d.data +} + +func (d document) SigningKey() jwk.Key { + return d.signingKey +} + +func (d document) SigningKeyID() string { + return d.signingKeyID +} + +func (d document) SigningTime() time.Time { + return d.signingTime +} + +func (d document) PayloadType() string { + return d.payloadType +} + +func (d document) Payload() hash.SHA256Hash { + return d.payload +} + +func (d document) Previous() []hash.SHA256Hash { + return append(d.prevs) +} + +func (d document) Ref() hash.SHA256Hash { + return d.ref +} + +func (d document) Version() Version { + return d.version +} + +func (d document) TimelineID() hash.SHA256Hash { + return d.timelineID +} + +func (d document) TimelineVersion() int { + return d.timelineVersion +} + +func (d document) VerifySignature(_ func(string) crypto.PublicKey) error { + return errors.New("not implemented") +} + +func (d *document) setData(data []byte) { + d.data = data + d.ref = hash.SHA256Sum(d.data) +} diff --git a/network/dag/document_test.go b/network/dag/document_test.go new file mode 100644 index 0000000000..ed38912fa7 --- /dev/null +++ b/network/dag/document_test.go @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "encoding/json" + hash2 "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestNewDocument(t *testing.T) { + payloadHash, _ := hash2.ParseHex("452d9e89d5bd5d9225fb6daecd579e7388a166c7661ca04e47fd3cd8446e4620") + t.Run("ok", func(t *testing.T) { + hash, _ := hash2.ParseHex("452d9e89d5bd5d9225fb6daecd579e7388a166c7661ca04e47fd3cd8446e4620") + + document, err := NewDocument(payloadHash, "some/type", []hash2.SHA256Hash{hash}) + + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "some/type", document.PayloadType()) + assert.Equal(t, document.Payload(), payloadHash) + assert.Equal(t, []hash2.SHA256Hash{hash}, document.Previous()) + assert.Equal(t, Version(1), document.Version()) + }) + t.Run("error - type empty", func(t *testing.T) { + document, err := NewDocument(payloadHash, "", nil) + assert.EqualError(t, err, errInvalidPayloadType.Error()) + assert.Nil(t, document) + }) + t.Run("error - type not a MIME type", func(t *testing.T) { + document, err := NewDocument(payloadHash, "foo", nil) + assert.EqualError(t, err, errInvalidPayloadType.Error()) + assert.Nil(t, document) + }) + t.Run("error - invalid prev", func(t *testing.T) { + document, err := NewDocument(payloadHash, "foo/bar", []hash2.SHA256Hash{hash2.EmptyHash()}) + assert.EqualError(t, err, errInvalidPrevs.Error()) + assert.Nil(t, document) + }) +} + +func Test_document_Getters(t *testing.T) { + payload, _ := hash2.ParseHex("452d9e89d5bd5d9225fb6daecd579e7388a166c7661ca04e47fd3cd8446e4620") + timelineID, _ := hash2.ParseHex("f33b5cae968cb88f157999b3551ab0863d2a8f0b") + prev1, _ := hash2.ParseHex("3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986") + prev2, _ := hash2.ParseHex("b3f2c3c396da1a949d214e4c2fe0fc9fb5f2a68ff1860df4ef10c9835e62e7c1") + doc := document{ + prevs: []hash2.SHA256Hash{prev1, prev2}, + payload: payload, + payloadType: "foo/bar", + signingTime: time.Unix(1023323333, 0), + version: 10, + timelineID: timelineID, + timelineVersion: 10, + } + doc.setData([]byte{1, 2, 3}) + + assert.Equal(t, doc.prevs, doc.Previous()) + assert.Equal(t, hash2.SHA256Hash(doc.payload), doc.Payload()) + assert.Equal(t, doc.payloadType, doc.PayloadType()) + assert.Equal(t, doc.signingTime, doc.SigningTime()) + assert.Equal(t, doc.version, doc.Version()) + assert.Equal(t, doc.timelineID, doc.TimelineID()) + assert.Equal(t, doc.timelineVersion, doc.TimelineVersion()) + assert.Equal(t, doc.data, doc.Data()) + assert.False(t, doc.Ref().Empty()) +} + +func Test_document_MarshalJSON(t *testing.T) { + expected := CreateTestDocument(1) + data, err := json.Marshal(expected) + if !assert.NoError(t, err) { + return + } + actual, err := UnmarshalJSON(data) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, expected, actual) +} + +func Test_document_VerifySignature(t *testing.T) { + t.Run("not implemented yet", func(t *testing.T) { + err := document{}.VerifySignature(nil) + assert.EqualError(t, err, "not implemented") + }) +} + +func generateKey() *ecdsa.PrivateKey { + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + return key +} + +func generateRSAKey() *rsa.PrivateKey { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + return key +} diff --git a/network/dag/dotviz_test.go b/network/dag/dotviz_test.go new file mode 100644 index 0000000000..67b16a7469 --- /dev/null +++ b/network/dag/dotviz_test.go @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "fmt" + "strings" +) + +// DotGraphVisitor is a graph visitor that outputs the walked path as "dot" diagram. +// The is currently unused, but can be used to debug the DAG to see how documents relate to each other. The output +// can be viewed using a DOT plugin or when rendering the DOT file to SVG/PNG, etc. +type DotGraphVisitor struct { + output string + aliases map[string]string + counter int + labelStyle LabelStyle + showAliases bool + showContent bool + + nodes []string + edges []string +} + +// LabelStyle defines node label styles for DotGraphVisitor. +type LabelStyle int + +const ( + // ShowAliasLabelStyle is a style that uses integer aliases for node labels. + ShowAliasLabelStyle LabelStyle = iota + // ShowAliasLabelStyle is a style that uses the references of nodes as label. + ShowRefLabelStyle LabelStyle = iota +) + +func NewDotGraphVisitor(labelStyle LabelStyle) *DotGraphVisitor { + return &DotGraphVisitor{ + aliases: map[string]string{}, + labelStyle: labelStyle, + } +} + +func (d *DotGraphVisitor) Accept(document Document) { + d.counter++ + d.nodes = append(d.nodes, fmt.Sprintf(" \"%s\"[label=\"%s (%d)\"]", document.Ref().String(), d.label(document), d.counter)) + for _, prev := range document.Previous() { + d.edges = append(d.edges, fmt.Sprintf(" \"%s\" -> \"%s\"", prev.String(), document.Ref().String())) + } +} + +func (d *DotGraphVisitor) Render() string { + var lines []string + lines = append(lines, "digraph {") + lines = append(lines, d.nodes...) + lines = append(lines, d.edges...) + lines = append(lines, "}") + return strings.Join(lines, "\n") +} + +func (d *DotGraphVisitor) label(document Document) string { + switch d.labelStyle { + case ShowAliasLabelStyle: + if alias, ok := d.aliases[document.Ref().String()]; ok { + return alias + } else { + alias = fmt.Sprintf("%d", len(d.aliases)+1) + d.aliases[document.Ref().String()] = alias + return alias + } + case ShowRefLabelStyle: + fallthrough + default: + return document.Ref().String() + } +} diff --git a/network/dag/mock.go b/network/dag/mock.go new file mode 100644 index 0000000000..df9cc7aa8d --- /dev/null +++ b/network/dag/mock.go @@ -0,0 +1,320 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: network/dag/dag.go + +// Package dag is a generated GoMock package. +package dag + +import ( + gomock "github.com/golang/mock/gomock" + hash "github.com/nuts-foundation/nuts-node/crypto/hash" + reflect "reflect" +) + +// MockDAG is a mock of DAG interface +type MockDAG struct { + ctrl *gomock.Controller + recorder *MockDAGMockRecorder +} + +// MockDAGMockRecorder is the mock recorder for MockDAG +type MockDAGMockRecorder struct { + mock *MockDAG +} + +// NewMockDAG creates a new mock instance +func NewMockDAG(ctrl *gomock.Controller) *MockDAG { + mock := &MockDAG{ctrl: ctrl} + mock.recorder = &MockDAGMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockDAG) EXPECT() *MockDAGMockRecorder { + return m.recorder +} + +// Subscribe mocks base method +func (m *MockDAG) Subscribe(documentType string, receiver Receiver) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Subscribe", documentType, receiver) +} + +// Subscribe indicates an expected call of Subscribe +func (mr *MockDAGMockRecorder) Subscribe(documentType, receiver interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockDAG)(nil).Subscribe), documentType, receiver) +} + +// Add mocks base method +func (m *MockDAG) Add(documents ...Document) error { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range documents { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Add", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Add indicates an expected call of Add +func (mr *MockDAGMockRecorder) Add(documents ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockDAG)(nil).Add), documents...) +} + +// MissingDocuments mocks base method +func (m *MockDAG) MissingDocuments() []hash.SHA256Hash { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MissingDocuments") + ret0, _ := ret[0].([]hash.SHA256Hash) + return ret0 +} + +// MissingDocuments indicates an expected call of MissingDocuments +func (mr *MockDAGMockRecorder) MissingDocuments() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MissingDocuments", reflect.TypeOf((*MockDAG)(nil).MissingDocuments)) +} + +// Walk mocks base method +func (m *MockDAG) Walk(walker Walker, visitor Visitor, startAt hash.SHA256Hash) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Walk", walker, visitor, startAt) + ret0, _ := ret[0].(error) + return ret0 +} + +// Walk indicates an expected call of Walk +func (mr *MockDAGMockRecorder) Walk(walker, visitor, startAt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Walk", reflect.TypeOf((*MockDAG)(nil).Walk), walker, visitor, startAt) +} + +// Root mocks base method +func (m *MockDAG) Root() (hash.SHA256Hash, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Root") + ret0, _ := ret[0].(hash.SHA256Hash) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Root indicates an expected call of Root +func (mr *MockDAGMockRecorder) Root() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Root", reflect.TypeOf((*MockDAG)(nil).Root)) +} + +// Get mocks base method +func (m *MockDAG) Get(ref hash.SHA256Hash) (Document, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ref) + ret0, _ := ret[0].(Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get +func (mr *MockDAGMockRecorder) Get(ref interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDAG)(nil).Get), ref) +} + +// GetByPayloadHash mocks base method +func (m *MockDAG) GetByPayloadHash(payloadHash hash.SHA256Hash) ([]Document, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByPayloadHash", payloadHash) + ret0, _ := ret[0].([]Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByPayloadHash indicates an expected call of GetByPayloadHash +func (mr *MockDAGMockRecorder) GetByPayloadHash(payloadHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByPayloadHash", reflect.TypeOf((*MockDAG)(nil).GetByPayloadHash), payloadHash) +} + +// All mocks base method +func (m *MockDAG) All() ([]Document, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "All") + ret0, _ := ret[0].([]Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// All indicates an expected call of All +func (mr *MockDAGMockRecorder) All() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockDAG)(nil).All)) +} + +// IsPresent mocks base method +func (m *MockDAG) IsPresent(ref hash.SHA256Hash) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsPresent", ref) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsPresent indicates an expected call of IsPresent +func (mr *MockDAGMockRecorder) IsPresent(ref interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPresent", reflect.TypeOf((*MockDAG)(nil).IsPresent), ref) +} + +// Heads mocks base method +func (m *MockDAG) Heads() []hash.SHA256Hash { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Heads") + ret0, _ := ret[0].([]hash.SHA256Hash) + return ret0 +} + +// Heads indicates an expected call of Heads +func (mr *MockDAGMockRecorder) Heads() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Heads", reflect.TypeOf((*MockDAG)(nil).Heads)) +} + +// MockPublisher is a mock of Publisher interface +type MockPublisher struct { + ctrl *gomock.Controller + recorder *MockPublisherMockRecorder +} + +// MockPublisherMockRecorder is the mock recorder for MockPublisher +type MockPublisherMockRecorder struct { + mock *MockPublisher +} + +// NewMockPublisher creates a new mock instance +func NewMockPublisher(ctrl *gomock.Controller) *MockPublisher { + mock := &MockPublisher{ctrl: ctrl} + mock.recorder = &MockPublisherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockPublisher) EXPECT() *MockPublisherMockRecorder { + return m.recorder +} + +// Subscribe mocks base method +func (m *MockPublisher) Subscribe(documentType string, receiver Receiver) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Subscribe", documentType, receiver) +} + +// Subscribe indicates an expected call of Subscribe +func (mr *MockPublisherMockRecorder) Subscribe(documentType, receiver interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockPublisher)(nil).Subscribe), documentType, receiver) +} + +// MockWalker is a mock of Walker interface +type MockWalker struct { + ctrl *gomock.Controller + recorder *MockWalkerMockRecorder +} + +// MockWalkerMockRecorder is the mock recorder for MockWalker +type MockWalkerMockRecorder struct { + mock *MockWalker +} + +// NewMockWalker creates a new mock instance +func NewMockWalker(ctrl *gomock.Controller) *MockWalker { + mock := &MockWalker{ctrl: ctrl} + mock.recorder = &MockWalkerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockWalker) EXPECT() *MockWalkerMockRecorder { + return m.recorder +} + +// walk mocks base method +func (m *MockWalker) walk(visitor Visitor, startAt hash.SHA256Hash, getFn func(hash.SHA256Hash) (Document, error), nextsFn func(hash.SHA256Hash) ([]hash.SHA256Hash, error), numberOfNodes int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "walk", visitor, startAt, getFn, nextsFn, numberOfNodes) + ret0, _ := ret[0].(error) + return ret0 +} + +// walk indicates an expected call of walk +func (mr *MockWalkerMockRecorder) walk(visitor, startAt, getFn, nextsFn, numberOfNodes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "walk", reflect.TypeOf((*MockWalker)(nil).walk), visitor, startAt, getFn, nextsFn, numberOfNodes) +} + +// MockPayloadStore is a mock of PayloadStore interface +type MockPayloadStore struct { + ctrl *gomock.Controller + recorder *MockPayloadStoreMockRecorder +} + +// MockPayloadStoreMockRecorder is the mock recorder for MockPayloadStore +type MockPayloadStoreMockRecorder struct { + mock *MockPayloadStore +} + +// NewMockPayloadStore creates a new mock instance +func NewMockPayloadStore(ctrl *gomock.Controller) *MockPayloadStore { + mock := &MockPayloadStore{ctrl: ctrl} + mock.recorder = &MockPayloadStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockPayloadStore) EXPECT() *MockPayloadStoreMockRecorder { + return m.recorder +} + +// IsPayloadPresent mocks base method +func (m *MockPayloadStore) IsPayloadPresent(payloadHash hash.SHA256Hash) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsPayloadPresent", payloadHash) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsPayloadPresent indicates an expected call of IsPayloadPresent +func (mr *MockPayloadStoreMockRecorder) IsPayloadPresent(payloadHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPayloadPresent", reflect.TypeOf((*MockPayloadStore)(nil).IsPayloadPresent), payloadHash) +} + +// ReadPayload mocks base method +func (m *MockPayloadStore) ReadPayload(payloadHash hash.SHA256Hash) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadPayload", payloadHash) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadPayload indicates an expected call of ReadPayload +func (mr *MockPayloadStoreMockRecorder) ReadPayload(payloadHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPayload", reflect.TypeOf((*MockPayloadStore)(nil).ReadPayload), payloadHash) +} + +// WritePayload mocks base method +func (m *MockPayloadStore) WritePayload(payloadHash hash.SHA256Hash, data []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WritePayload", payloadHash, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// WritePayload indicates an expected call of WritePayload +func (mr *MockPayloadStoreMockRecorder) WritePayload(payloadHash, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WritePayload", reflect.TypeOf((*MockPayloadStore)(nil).WritePayload), payloadHash, data) +} diff --git a/network/dag/parser.go b/network/dag/parser.go new file mode 100644 index 0000000000..b842532faf --- /dev/null +++ b/network/dag/parser.go @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jws" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "time" +) + +// UnmarshalJSON unmarshals a document in compact serialization format from JSON. +func UnmarshalJSON(input []byte) (Document, error) { + str := "" + if err := json.Unmarshal(input, &str); err != nil { + return nil, err + } + return ParseDocument([]byte(str)) +} + +// ParseDocument parses the input as Nuts Network Document according to RFC004. +func ParseDocument(input []byte) (Document, error) { + message, err := jws.Parse(bytes.NewReader(input)) + if err != nil { + return nil, fmt.Errorf(unableToParseDocumentErrFmt, err) + } + if len(message.Signatures()) == 0 { + return nil, documentValidationError("JWS does not contain any signature") + } + signature := message.Signatures()[0] + headers := signature.ProtectedHeaders() + + var steps = []documentParseStep{ + parseSigningAlgorithm, + parsePayload, + parseContentType, + parseSigningKey, + parseSigningTime, + parseVersion, + parsePrevious, + parseTimelineID, + parseTimelineVersion, + } + + result := &document{} + result.setData(input) + for _, step := range steps { + if err := step(result, headers, message); err != nil { + return nil, err + } + } + return result, nil +} + +func documentValidationError(format string, args ...interface{}) error { + return fmt.Errorf(documentNotValidErrFmt, fmt.Errorf(format, args...)) +} + +// documentParseStep defines a function that parses a part of a JWS, building the internal representation of a document. +// If an error occurs during parsing or validation it should be returned. +type documentParseStep func(document *document, headers jws.Headers, message *jws.Message) error + +// parseSigningAlgorithm validates whether the signing algorithm is allowed +func parseSigningAlgorithm(_ *document, headers jws.Headers, _ *jws.Message) error { + if !isAlgoAllowed(headers.Algorithm()) { + return documentValidationError("signing algorithm not allowed: %s", headers.Algorithm()) + } + return nil +} + +// parsePayload parses the document payload (contents) and sets it +func parsePayload(document *document, _ jws.Headers, message *jws.Message) error { + payload, err := hash.ParseHex(string(message.Payload())) + if err != nil { + return documentValidationError("invalid payload: %w", err) + } + document.payload = payload + return nil +} + +// parseContentType parses, validates and sets the document payload content type. +func parseContentType(document *document, headers jws.Headers, _ *jws.Message) error { + contentType := headers.ContentType() + if !ValidatePayloadType(contentType) { + return documentValidationError(errInvalidPayloadType.Error()) + } + document.payloadType = contentType + return nil +} + +// parseSigningKey parses, validates and sets the document signing key (`jwk`) or key ID (`kid`). +func parseSigningKey(document *document, headers jws.Headers, _ *jws.Message) error { + if key, ok := headers.Get(jws.JWKKey); ok { + document.signingKey = key.(jwk.Key) + } else if kid, ok := headers.Get(jws.KeyIDKey); ok { + document.signingKeyID = kid.(string) + } else { + return documentValidationError("either `kid` or `jwk` header must be present") + } + return nil +} + +// parseSigningTime parses, validates and sets the document signing time. +func parseSigningTime(document *document, headers jws.Headers, _ *jws.Message) error { + if timeAsInterf, ok := headers.Get(signingTimeHeader); !ok { + return documentValidationError(missingHeaderErrFmt, signingTimeHeader) + } else if timeAsFloat64, ok := timeAsInterf.(float64); !ok { + return documentValidationError(invalidHeaderErrFmt, signingTimeHeader) + } else { + document.signingTime = time.Unix(int64(timeAsFloat64), 0).UTC() + return nil + } +} + +// parseVersion parses, validates and sets the document format version. +func parseVersion(document *document, headers jws.Headers, _ *jws.Message) error { + var version Version + if versionAsInterf, ok := headers.Get(versionHeader); !ok { + return documentValidationError(missingHeaderErrFmt, versionHeader) + } else if versionAsFloat64, ok := versionAsInterf.(float64); !ok { + return documentValidationError(invalidHeaderErrFmt, versionHeader) + } else if version = Version(versionAsFloat64); version != currentVersion { + return documentValidationError("unsupported version: %d", version) + } else { + document.version = version + return nil + } +} + +// parsePrevious parses, validates and sets the document prevs fields. +func parsePrevious(document *document, headers jws.Headers, _ *jws.Message) error { + if prevsAsInterf, ok := headers.Get(previousHeader); !ok { + return documentValidationError(missingHeaderErrFmt, previousHeader) + } else if prevsAsSlice, ok := prevsAsInterf.([]interface{}); !ok { + return documentValidationError(invalidHeaderErrFmt, previousHeader) + } else { + for _, prevAsInterf := range prevsAsSlice { + if prevAsString, ok := prevAsInterf.(string); !ok { + return documentValidationError(invalidHeaderErrFmt, previousHeader) + } else if prev, err := hash.ParseHex(prevAsString); err != nil { + return documentValidationError(invalidHeaderErrFmt, previousHeader) + } else { + document.prevs = append(document.prevs, prev) + } + } + return nil + } +} + +// parseTimelineID parses, validates and sets the document timeline ID field. +func parseTimelineID(document *document, headers jws.Headers, _ *jws.Message) error { + if tidAsInterf, _ := headers.Get(timelineIDHeader); tidAsInterf != nil { + if tidAsString, ok := tidAsInterf.(string); !ok { + return documentValidationError(invalidHeaderErrFmt, timelineIDHeader) + } else if timelineID, err := hash.ParseHex(tidAsString); err != nil { + return documentValidationError(invalidHeaderErrFmt+": %w", timelineIDHeader, err) + } else { + document.timelineID = timelineID + } + } + return nil +} + +// parseTimelineVersion parses, validates and sets the document timeline version field. Timeline ID must be present when +// timeline version is present. +func parseTimelineVersion(document *document, headers jws.Headers, _ *jws.Message) error { + tivAsInterf, _ := headers.Get(timelineVersionHeader) + if tivAsInterf == nil { + return nil + } + if tiv, ok := tivAsInterf.(float64); !ok { + return documentValidationError(invalidHeaderErrFmt, timelineVersionHeader) + } else if tiv < 0 { + return documentValidationError(invalidHeaderErrFmt, timelineVersionHeader) + } else if document.timelineID.Empty() { + return documentValidationError("%s specified without %s header", timelineVersionHeader, timelineIDHeader) + } else if tiv != float64(int(tiv)) { + return documentValidationError(invalidHeaderErrFmt, timelineVersionHeader) + } else { + document.timelineVersion = int(tiv) + return nil + } +} + +func isAlgoAllowed(algo jwa.SignatureAlgorithm) bool { + for _, current := range allowedAlgos { + if algo == current { + return true + } + } + return false +} diff --git a/network/dag/parser_test.go b/network/dag/parser_test.go new file mode 100644 index 0000000000..f8e7711c53 --- /dev/null +++ b/network/dag/parser_test.go @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "crypto" + "crypto/ecdsa" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jws" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestParseDocument(t *testing.T) { + key := generateKey() + payload, _ := hash.ParseHex("3d2c482831de294af919a4c4604c97156cf0ba46fcf6f96e50774597470f8db8") + payloadAsBytes := []byte(payload.String()) + t.Run("ok", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + if !assert.NoError(t, err) { + return + } + + var actualKey ecdsa.PublicKey + err = document.SigningKey().Raw(&actualKey) + if !assert.NoError(t, err) { + return + } + assert.NotNil(t, document) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, payload, document.Payload()) + assert.Equal(t, key.PublicKey, actualKey) + assert.Equal(t, 1, int(document.Version())) + assert.Equal(t, "foo/bar", document.PayloadType()) + assert.Equal(t, time.UTC, document.SigningTime().Location()) + assert.Equal(t, headers.PrivateParams()[previousHeader].([]string)[0], document.Previous()[0].String()) + assert.Equal(t, headers.PrivateParams()[timelineIDHeader].(string), document.TimelineID().String()) + assert.Equal(t, headers.PrivateParams()[timelineVersionHeader].(int), document.TimelineVersion()) + assert.NotNil(t, document.Data()) + assert.False(t, document.Ref().Empty()) + }) + t.Run("error - input not a JWS (compact serialization format)", func(t *testing.T) { + document, err := ParseDocument([]byte("not a JWS")) + assert.Nil(t, document) + assert.Contains(t, err.Error(), "unable to parse document: failed to parse jws message: invalid compact serialization format") + }) + t.Run("error - input not a JWS (JSON serialization format)", func(t *testing.T) { + document, err := ParseDocument([]byte("{}")) + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: JWS does not contain any signature") + }) + t.Run("error - sigt header is missing", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + delete(headers.PrivateParams(), signingTimeHeader) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: missing sigt header") + }) + t.Run("error - invalid sigt header", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(signingTimeHeader, "not a date") + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: invalid sigt header") + }) + t.Run("error - vers header is missing", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + delete(headers.PrivateParams(), versionHeader) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: missing ver header") + }) + t.Run("error - prevs header is missing", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + delete(headers.PrivateParams(), previousHeader) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: missing prevs header") + }) + t.Run("error - invalid prevs (not an array)", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(previousHeader, 2) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: invalid prevs header") + }) + t.Run("error - invalid prevs (invalid entry)", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(previousHeader, []string{"not a hash"}) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: invalid prevs header") + }) + t.Run("error - invalid prevs (invalid entry, not a string)", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(previousHeader, []int{5}) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: invalid prevs header") + }) + t.Run("error - cty header is invalid", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(jws.ContentTypeKey, "") + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: payload type must be formatted as MIME type") + }) + t.Run("error - invalid version", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(versionHeader, "foobar") + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: invalid ver header") + }) + t.Run("error - unsupported version", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(versionHeader, 2) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: unsupported version: 2") + }) + t.Run("error - invalid algorithm", func(t *testing.T) { + key := generateRSAKey() + headers := makeJWSHeaders(key, "") + headers.Set(jws.AlgorithmKey, jwa.RS256) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.EqualError(t, err, "document validation failed: signing algorithm not allowed: RS256") + }) + t.Run("error - invalid tid header", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(timelineIDHeader, "not a valid hash") + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: invalid tid header") + }) + t.Run("error - invalid tid header (not a string)", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(timelineIDHeader, 5) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: invalid tid header") + }) + t.Run("error - invalid tiv header (not a numeric value)", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(timelineVersionHeader, "not a numeric value") + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: invalid tiv header") + }) + t.Run("error - invalid tiv header (not a numeric value)", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(timelineVersionHeader, "not a numeric value") + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: invalid tiv header") + }) + t.Run("error - invalid tiv header (value smaller than 0)", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(timelineVersionHeader, -1) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: invalid tiv header") + }) + t.Run("error - invalid tiv header (value non-integer)", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + headers.Set(timelineVersionHeader, 1.5) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: invalid tiv header") + }) + t.Run("error - tiv header without tid", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + delete(headers.PrivateParams(), timelineIDHeader) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: tiv specified without tid header") + }) + t.Run("error - tiv header without tid", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + delete(headers.PrivateParams(), timelineIDHeader) + signature, _ := jws.Sign(payloadAsBytes, headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: tiv specified without tid header") + }) + t.Run("error - invalid payload", func(t *testing.T) { + headers := makeJWSHeaders(key, "") + signature, _ := jws.Sign([]byte("not a valid hash"), headers.Algorithm(), key, jws.WithHeaders(headers)) + + document, err := ParseDocument(signature) + + assert.Nil(t, document) + assert.Contains(t, err.Error(), "document validation failed: invalid payload") + }) +} + +func makeJWSHeaders(key crypto.Signer, kid string) jws.Headers { + prev, _ := hash.ParseHex("bedcd5bfb50af622be56c4aec7ac5da64745686b362afc7e615ea89b0705b8f8") + timelineID, _ := hash.ParseHex("a7da489976d0047490617adb4f7a1f27f7af8b52a5176fd002ffe471863520ab") + headerMap := map[string]interface{}{ + jws.AlgorithmKey: jwa.ES256, + jws.ContentTypeKey: "foo/bar", + jws.CriticalKey: []string{signingTimeHeader, versionHeader, previousHeader}, + signingTimeHeader: time.Now().UTC().Unix(), + versionHeader: 1, + previousHeader: []string{prev.String()}, + timelineIDHeader: timelineID.String(), + timelineVersionHeader: 1, + } + if key != nil { + keyAsJWS, _ := jwk.New(key.Public()) + headerMap[jws.JWKKey] = keyAsJWS + } + if kid != "" { + headerMap[jws.KeyIDKey] = kid + } + headers := jws.NewHeaders() + for key, value := range headerMap { + if err := headers.Set(key, value); err != nil { + logrus.Fatalf("Unable to set header %s: %v", key, err) + } + } + return headers +} diff --git a/network/dag/signing.go b/network/dag/signing.go new file mode 100644 index 0000000000..04d2e3c525 --- /dev/null +++ b/network/dag/signing.go @@ -0,0 +1,114 @@ +package dag + +import ( + "errors" + "fmt" + "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jws" + "github.com/nuts-foundation/nuts-node/crypto" + "time" +) + +const errSigningDocumentFmt = "error while signing document: %w" + +// DocumentSigner defines functions to sign documents. +type DocumentSigner interface { + // Sign signs the unsigned document, including the signingTime parameter as header. + Sign(input UnsignedDocument, signingTime time.Time) (Document, error) +} + +// NewAttachedJWKDocumentSigner creates a DocumentSigner that signs the document using the given key. +// The public key (identified by `kid`) is added to the signed document as `jwk` header. The public key is resolved +// using the given resolver and the `kid` parameter. +func NewAttachedJWKDocumentSigner(jwsSigner crypto.JWSSigner, kid string, keyResolver crypto.KeyResolver) DocumentSigner { + return &documentSigner{ + signer: jwsSigner, + kid: kid, + attach: true, + resolver: keyResolver, + } +} + +// NewDocumentSigner creates a DocumentSigner that signs the document using the given key. +// The public key is not included in the signed document, instead the `kid` header is added which must refer to the ID +// of the used key. +func NewDocumentSigner(jwsSigner crypto.JWSSigner, kid string) DocumentSigner { + return &documentSigner{ + signer: jwsSigner, + kid: kid, + attach: false, + } +} + +type documentSigner struct { + attach bool + kid string + signer crypto.JWSSigner + resolver crypto.KeyResolver +} + +func (d documentSigner) Sign(input UnsignedDocument, signingTime time.Time) (Document, error) { + doc, ok := input.(*document) + if !ok { + return nil, errors.New("unsupported document") + } + // Preliminary sanity checks + if signingTime.IsZero() { + return nil, errors.New("signing time is zero") + } + if !doc.signingTime.IsZero() { + return nil, errors.New("document is already signed") + } + + var key jwk.Key + if d.attach { + keyAsPublicKey, err := d.resolver.GetPublicKey(d.kid) + if err != nil { + return nil, fmt.Errorf(errSigningDocumentFmt, err) + } + key, err = jwk.New(keyAsPublicKey) + if err != nil { + return nil, fmt.Errorf(errSigningDocumentFmt, err) + } + } + + prevsAsString := make([]string, len(doc.prevs)) + for i, prev := range doc.prevs { + prevsAsString[i] = prev.String() + } + normalizedMoment := signingTime.UTC() + headerMap := map[string]interface{}{ + jws.ContentTypeKey: doc.payloadType, + jws.CriticalKey: []string{signingTimeHeader, versionHeader, previousHeader}, + signingTimeHeader: normalizedMoment.Unix(), + previousHeader: prevsAsString, + versionHeader: doc.Version(), + } + if d.attach { + headerMap[jws.CriticalKey] = append(headerMap[jws.CriticalKey].([]string), jws.JWKKey) + headerMap[jws.JWKKey] = key + } else { + headerMap[jws.CriticalKey] = append(headerMap[jws.CriticalKey].([]string), jws.KeyIDKey) + headerMap[jws.KeyIDKey] = d.kid + } + + if !doc.timelineID.Empty() { + headerMap[timelineIDHeader] = doc.timelineID + if doc.timelineVersion > 0 { + headerMap[timelineVersionHeader] = doc.timelineVersion + } + } + + data, err := d.signer.SignJWS([]byte(doc.payload.String()), headerMap, d.kid) + if err != nil { + return nil, fmt.Errorf(errSigningDocumentFmt, err) + } + doc.setData([]byte(data)) + doc.signingTime = time.Unix(normalizedMoment.Unix(), 0).UTC() + if d.attach { + doc.signingKey = key + } else { + doc.signingKeyID = d.kid + } + return doc, nil +} diff --git a/network/dag/signing_test.go b/network/dag/signing_test.go new file mode 100644 index 0000000000..1c6cdb3bed --- /dev/null +++ b/network/dag/signing_test.go @@ -0,0 +1,86 @@ +package dag + +import ( + "crypto" + "crypto/sha1" + "encoding/base32" + "github.com/lestrrat-go/jwx/jws" + hash2 "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestDocumentSigner(t *testing.T) { + payloadHash, _ := hash2.ParseHex("452d9e89d5bd5d9225fb6daecd579e7388a166c7661ca04e47fd3cd8446e4620") + key := generateKey() + kidAsArray := sha1.Sum(key.X.Bytes()) + kid := base32.HexEncoding.EncodeToString(kidAsArray[:]) + prev1, _ := hash2.ParseHex("3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986") + prev2, _ := hash2.ParseHex("b3f2c3c396da1a949d214e4c2fe0fc9fb5f2a68ff1860df4ef10c9835e62e7c1") + expectedPrevs := []hash2.SHA256Hash{prev1, prev2} + contentType := "foo/bar" + moment := time.Date(2020, 10, 23, 13, 0, 0, 0, time.FixedZone("test", 1)) + t.Run("ok - attach key", func(t *testing.T) { + doc, err := NewDocument(payloadHash, contentType, expectedPrevs) + if !assert.NoError(t, err) { + return + } + + signer := &trackingJWSSigner{} + signedDoc, err := NewAttachedJWKDocumentSigner(signer, kid, &simpleKeyResolver{key: key}).Sign(doc, moment) + if !assert.NoError(t, err) { + return + } + // JWS headers + assert.Equal(t, contentType, signer.headers[jws.ContentTypeKey]) + assert.Empty(t, signer.headers[jws.KeyIDKey]) + // Custom headers + assert.Equal(t, int64(1603457999), signer.headers[signingTimeHeader].(int64)) + assert.Equal(t, 1, int(signer.headers[versionHeader].(Version))) + prevs := signer.headers[previousHeader] + assert.Len(t, prevs, 2, "expected 2 prevs") + assert.Equal(t, prev1.String(), prevs.([]string)[0]) + assert.Equal(t, prev2.String(), prevs.([]string)[1]) + // Resulting doc + assert.Equal(t, "fine JWS", string(signedDoc.Data())) + assert.False(t, signedDoc.Ref().Empty()) + assert.Equal(t, time.UTC, signedDoc.SigningTime().Location()) + }) + t.Run("ok - with kid", func(t *testing.T) { + doc, err := NewDocument(payloadHash, contentType, expectedPrevs) + if !assert.NoError(t, err) { + return + } + + signer := &trackingJWSSigner{} + signedDoc, err := NewDocumentSigner(signer, kid).Sign(doc, moment) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, kid, signer.headers[jws.KeyIDKey]) + assert.Nil(t, signer.headers[jws.JWKKey]) + assert.Equal(t, "fine JWS", string(signedDoc.Data())) + }) +} + +type simpleKeyResolver struct { + key crypto.PublicKey +} + +func (s simpleKeyResolver) GetPublicKey(_ string) (crypto.PublicKey, error) { + return s.key, nil +} + +type trackingJWSSigner struct { + headers map[string]interface{} + payload []byte + kid string +} + +func (t *trackingJWSSigner) SignJWS(payload []byte, protectedHeaders map[string]interface{}, kid string) (string, error) { + t.payload = payload + t.headers = protectedHeaders + t.kid = kid + return "fine JWS", nil +} diff --git a/network/dag/test.go b/network/dag/test.go new file mode 100644 index 0000000000..4855ae6e27 --- /dev/null +++ b/network/dag/test.go @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package dag + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/binary" + "fmt" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jws" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "time" +) + +var privateKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + +// CreateTestDocument creates a document witth the given num as payload hash and signs it with a random EC key. +func CreateTestDocument(num uint32, prevs ...hash.SHA256Hash) Document { + payloadHash := hash.SHA256Hash{} + binary.BigEndian.PutUint32(payloadHash[hash.SHA256HashSize-4:], num) + unsignedDocument, _ := NewDocument(payloadHash, "foo/bar", prevs) + signedDocument, err := NewDocumentSigner(&testSigner{}, fmt.Sprintf("%d", num)).Sign(unsignedDocument, time.Now()) + if err != nil { + panic(err) + } + return signedDocument +} + +type testSigner struct { + +} + +func (t testSigner) SignJWS(payload []byte, protectedHeaders map[string]interface{}, _ string) (string, error) { + privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + hdrs := jws.NewHeaders() + for k, v := range protectedHeaders { + if err := hdrs.Set(k, v); err != nil { + return "", err + } + } + sig, _ := jws.Sign(payload, jwa.ES256, privateKey, jws.WithHeaders(hdrs)) + return string(sig), nil +} + diff --git a/network/engine/engine.go b/network/engine/engine.go new file mode 100644 index 0000000000..fe26c54b07 --- /dev/null +++ b/network/engine/engine.go @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package engine + +import ( + "fmt" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto" + hash2 "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network" + "github.com/nuts-foundation/nuts-node/network/api/v1" + "github.com/nuts-foundation/nuts-node/network/log" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "time" +) + +var clientCreator = func() *v1.HTTPClient { + return &v1.HTTPClient{ + ServerAddress: core.NutsConfig().ServerAddress(), + Timeout: 30 * time.Second, + } +} + +// NewNetworkEngine returns the core definition for the network +func NewNetworkEngine(keyStore crypto.KeyStore) (*core.Engine, network.Network) { + config := network.DefaultConfig() + instance := network.NewNetworkInstance(config, keyStore) + return &core.Engine{ + Cmd: cmd(), + Configurable: instance, + Diagnosable: instance, + Runnable: instance, + Config: &instance.Config, + ConfigKey: "network", + FlagSet: flagSet(), + Routes: func(router core.EchoRouter) { + v1.RegisterHandlers(router, v1.ServerImplementation(instance)) + }, + Name: network.ModuleName, + }, instance +} + +func flagSet() *pflag.FlagSet { + defs := network.DefaultConfig() + flagSet := pflag.NewFlagSet("network", pflag.ContinueOnError) + flagSet.String("grpcAddr", defs.GrpcAddr, "Local address for gRPC to listen on. "+ + "If empty the gRPC server won't be started and other nodes will not be able to connect to this node "+ + "(outbound connections can still be made).") + flagSet.String("publicAddr", defs.PublicAddr, "Public address (of this node) other nodes can use to connect to it. If set, it is registered on the nodelist.") + flagSet.String("bootstrapNodes", defs.BootstrapNodes, "Space-separated list of bootstrap nodes (`:`) which the node initially connect to.") + flagSet.Bool("enableTLS", defs.EnableTLS, "Whether to enable TLS for inbound gRPC connections. "+ + "If set to `true` (which is default) `certFile` and `certKeyFile` MUST be configured.") + flagSet.String("certFile", defs.CertFile, "PEM file containing the server certificate for the gRPC server. "+ + "Required when `enableTLS` is `true`.") + flagSet.String("certKeyFile", defs.CertKeyFile, "PEM file containing the private key of the server certificate. "+ + "Required when `enableTLS` is `true`.") + flagSet.String("databaseFile", defs.DatabaseFile, "File path to the network database.") + flagSet.String("trustStoreFile", defs.TrustStoreFile, "PEM file containing the trusted CA certificates for authenticating remote gRPC servers.") + flagSet.Int("advertHashesInterval", defs.AdvertHashesInterval, "Interval (in milliseconds) that specifies how often the node should broadcast its last hashes to other nodes.") + return flagSet +} + +func cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "network", + Short: "network commands", + } + cmd.AddCommand(listCommand()) + cmd.AddCommand(getCommand()) + cmd.AddCommand(payloadCommand()) + return cmd +} + +func payloadCommand() *cobra.Command { + return &cobra.Command{ + Use: "payload [ref]", + Short: "Retrieves the payload of a document from the network", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + hash, err := hash2.ParseHex(args[0]) + if err != nil { + return err + } + data, err := clientCreator().GetDocumentPayload(hash) + if err != nil { + return err + } + if data == nil { + log.Logger().Warnf("Document or contents not found: %s", hash) + return nil + } + println(string(data)) + return nil + }, + } +} + +func getCommand() *cobra.Command { + return &cobra.Command{ + Use: "get [ref]", + Short: "Gets a document from the network", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + hash, err := hash2.ParseHex(args[0]) + if err != nil { + return err + } + document, err := clientCreator().GetDocument(hash) + if err != nil { + return err + } + if document == nil { + log.Logger().Warnf("Document not found: %s", hash) + return nil + } + fmt.Printf("Document %s:\n Type: %s\n Timestamp: %s\n", document.Ref(), document.PayloadType(), document.SigningTime()) + return nil + }, + } +} + +func listCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "Lists the documents on the network", + RunE: func(cmd *cobra.Command, args []string) error { + documents, err := clientCreator().ListDocuments() + if err != nil { + return err + } + const format = "%-65s %-40s %-20s\n" + fmt.Printf(format, "Hashes", "Timestamp", "Type") + for _, document := range documents { + fmt.Printf(format, document.Ref(), document.SigningTime(), document.PayloadType()) + } + return nil + }, + } +} diff --git a/network/engine/engine_test.go b/network/engine/engine_test.go new file mode 100644 index 0000000000..74928dc39a --- /dev/null +++ b/network/engine/engine_test.go @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package engine + +import ( + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + http2 "github.com/nuts-foundation/nuts-node/test/http" + "github.com/nuts-foundation/nuts-node/test/io" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestNewEngine(t *testing.T) { + testDirectory := io.TestDirectory(t) + e, i := NewNetworkEngine(crypto.NewTestCryptoInstance(testDirectory)) + assert.NotNil(t, e) + assert.NotNil(t, i) +} + +func TestFlagSet(t *testing.T) { + assert.NotNil(t, flagSet()) +} + +func TestCmd_List(t *testing.T) { + cmd := createCmd(t) + response := []interface{}{string(dag.CreateTestDocument(1).Data()), string(dag.CreateTestDocument(2).Data())} + s := httptest.NewServer(http2.Handler{StatusCode: http.StatusOK, ResponseData: response}) + os.Setenv("NUTS_ADDRESS", s.URL) + core.NutsConfig().Load(cmd) + defer s.Close() + + cmd.SetArgs([]string{"list"}) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCmd_Get(t *testing.T) { + cmd := createCmd(t) + response := dag.CreateTestDocument(1) + handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: string(response.Data())} + s := httptest.NewServer(handler) + os.Setenv("NUTS_ADDRESS", s.URL) + core.NutsConfig().Load(cmd) + defer s.Close() + + t.Run("ok", func(t *testing.T) { + cmd.SetArgs([]string{"get", response.Ref().String()}) + err := cmd.Execute() + assert.NoError(t, err) + }) + t.Run("not found", func(t *testing.T) { + handler.StatusCode = http.StatusNotFound + handler.ResponseData = []byte("not found") + cmd.SetArgs([]string{"get", response.Ref().String()}) + err := cmd.Execute() + assert.NoError(t, err) + }) +} + +func TestCmd_Payload(t *testing.T) { + cmd := createCmd(t) + handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: []byte("Hello, World!")} + s := httptest.NewServer(handler) + os.Setenv("NUTS_ADDRESS", s.URL) + core.NutsConfig().Load(cmd) + defer s.Close() + + t.Run("ok", func(t *testing.T) { + h := hash.SHA256Sum([]byte{1, 2, 3}) + cmd.SetArgs([]string{"payload", h.String()}) + err := cmd.Execute() + assert.NoError(t, err) + }) + t.Run("not found", func(t *testing.T) { + h := hash.SHA256Sum([]byte{1, 2, 3}) + cmd.SetArgs([]string{"payload", h.String()}) + err := cmd.Execute() + assert.NoError(t, err) + }) +} + +func createCmd(t *testing.T) *cobra.Command { + core.NutsConfig().Load(&cobra.Command{}) + testDirectory := io.TestDirectory(t) + cryptoInstance := crypto.NewTestCryptoInstance(testDirectory) + engine, _ := NewNetworkEngine(cryptoInstance) + return engine.Cmd +} diff --git a/network/interface.go b/network/interface.go new file mode 100644 index 0000000000..2122e43911 --- /dev/null +++ b/network/interface.go @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package network + +import ( + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + "time" +) + +// Network is the interface to be implemented by any remote or local client +type Network interface { + // Subscribe makes a subscription for the specified document type. The receiver is called when a document + // is received for the specified type. + Subscribe(documentType string, receiver dag.Receiver) + // GetDocumentPayload retrieves the document payload for the given document. If the document or payload is not found + // nil is returned. + GetDocumentPayload(documentRef hash.SHA256Hash) ([]byte, error) + // GetDocument retrieves the document for the given reference. If the document is not known, an error is returned. + GetDocument(documentRef hash.SHA256Hash) (dag.Document, error) + // CreateDocument creates a new document with the specified payload, and signs it using the specified key. + // If the key should be inside the document (instead of being referred to) `attachKey` should be true. + CreateDocument(payloadType string, payload []byte, signingKeyID string, attachKey bool, timestamp time.Time, fieldsOpts ...dag.FieldOpt) (dag.Document, error) + // ListDocuments returns all documents known to this NetworkEngine instance. + ListDocuments() ([]dag.Document, error) +} diff --git a/network/log/logger.go b/network/log/logger.go new file mode 100644 index 0000000000..3b60d72095 --- /dev/null +++ b/network/log/logger.go @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package log + +import ( + "github.com/sirupsen/logrus" +) + +var _logger = logrus.StandardLogger().WithField("module", "Network") + +// Logger returns the logger for the network engine. +func Logger() *logrus.Entry { + return _logger +} diff --git a/network/mock.go b/network/mock.go new file mode 100644 index 0000000000..2c7cabaa92 --- /dev/null +++ b/network/mock.go @@ -0,0 +1,113 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: network/interface.go + +// Package network is a generated GoMock package. +package network + +import ( + gomock "github.com/golang/mock/gomock" + hash "github.com/nuts-foundation/nuts-node/crypto/hash" + dag "github.com/nuts-foundation/nuts-node/network/dag" + reflect "reflect" + time "time" +) + +// MockNetwork is a mock of Network interface +type MockNetwork struct { + ctrl *gomock.Controller + recorder *MockNetworkMockRecorder +} + +// MockNetworkMockRecorder is the mock recorder for MockNetwork +type MockNetworkMockRecorder struct { + mock *MockNetwork +} + +// NewMockNetwork creates a new mock instance +func NewMockNetwork(ctrl *gomock.Controller) *MockNetwork { + mock := &MockNetwork{ctrl: ctrl} + mock.recorder = &MockNetworkMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockNetwork) EXPECT() *MockNetworkMockRecorder { + return m.recorder +} + +// Subscribe mocks base method +func (m *MockNetwork) Subscribe(documentType string, receiver dag.Receiver) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Subscribe", documentType, receiver) +} + +// Subscribe indicates an expected call of Subscribe +func (mr *MockNetworkMockRecorder) Subscribe(documentType, receiver interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockNetwork)(nil).Subscribe), documentType, receiver) +} + +// GetDocumentPayload mocks base method +func (m *MockNetwork) GetDocumentPayload(documentRef hash.SHA256Hash) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDocumentPayload", documentRef) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDocumentPayload indicates an expected call of GetDocumentPayload +func (mr *MockNetworkMockRecorder) GetDocumentPayload(documentRef interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDocumentPayload", reflect.TypeOf((*MockNetwork)(nil).GetDocumentPayload), documentRef) +} + +// GetDocument mocks base method +func (m *MockNetwork) GetDocument(documentRef hash.SHA256Hash) (dag.Document, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDocument", documentRef) + ret0, _ := ret[0].(dag.Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDocument indicates an expected call of GetDocument +func (mr *MockNetworkMockRecorder) GetDocument(documentRef interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDocument", reflect.TypeOf((*MockNetwork)(nil).GetDocument), documentRef) +} + +// CreateDocument mocks base method +func (m *MockNetwork) CreateDocument(payloadType string, payload []byte, signingKeyID string, attachKey bool, timestamp time.Time, fieldsOpts ...dag.FieldOpt) (dag.Document, error) { + m.ctrl.T.Helper() + varargs := []interface{}{payloadType, payload, signingKeyID, attachKey, timestamp} + for _, a := range fieldsOpts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateDocument", varargs...) + ret0, _ := ret[0].(dag.Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateDocument indicates an expected call of CreateDocument +func (mr *MockNetworkMockRecorder) CreateDocument(payloadType, payload, signingKeyID, attachKey, timestamp interface{}, fieldsOpts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{payloadType, payload, signingKeyID, attachKey, timestamp}, fieldsOpts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDocument", reflect.TypeOf((*MockNetwork)(nil).CreateDocument), varargs...) +} + +// ListDocuments mocks base method +func (m *MockNetwork) ListDocuments() ([]dag.Document, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListDocuments") + ret0, _ := ret[0].([]dag.Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListDocuments indicates an expected call of ListDocuments +func (mr *MockNetworkMockRecorder) ListDocuments() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDocuments", reflect.TypeOf((*MockNetwork)(nil).ListDocuments)) +} diff --git a/network/network.go b/network/network.go new file mode 100644 index 0000000000..e57ccfa95f --- /dev/null +++ b/network/network.go @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package network + +import ( + "crypto/tls" + "fmt" + "github.com/google/uuid" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + "github.com/nuts-foundation/nuts-node/network/log" + "github.com/nuts-foundation/nuts-node/network/p2p" + "github.com/nuts-foundation/nuts-node/network/proto" + "github.com/pkg/errors" + "sync" + "time" +) + +// ModuleName defines the name of this module +const ModuleName = "Network" + +// NetworkEngine implements Network interface and Engine functions. +// TODO: Refactor Engine-structs to something more sane or rename this struct (#18) +type NetworkEngine struct { + Config Config + configOnce sync.Once + p2pNetwork p2p.P2PNetwork + protocol proto.Protocol + documentGraph dag.DAG + payloadStore dag.PayloadStore + keyStore crypto.KeyStore +} + +// NewNetworkInstance creates a new NetworkEngine engine instance. +func NewNetworkInstance(config Config, keyStore crypto.KeyStore) *NetworkEngine { + result := &NetworkEngine{ + Config: config, + keyStore: keyStore, + p2pNetwork: p2p.NewP2PNetwork(), + protocol: proto.NewProtocol(), + } + return result +} + +// Configure configures the NetworkEngine subsystem +func (n *NetworkEngine) Configure() error { + var err error + n.configOnce.Do(func() { + if n.documentGraph, n.payloadStore, err = dag.NewBBoltDAG(n.Config.DatabaseFile); err != nil { + return + } + peerID := p2p.PeerID(uuid.New().String()) + n.protocol.Configure(n.p2pNetwork, n.documentGraph, n.payloadStore, time.Duration(n.Config.AdvertHashesInterval)*time.Millisecond, peerID) + networkConfig, p2pErr := n.buildP2PConfig(peerID) + if p2pErr != nil { + log.Logger().Warnf("Unable to build P2P layer config, NetworkEngine will be offline (reason: %v)", p2pErr) + return + } + err = n.p2pNetwork.Configure(*networkConfig) + }) + return err +} + +// Start initiates the NetworkEngine subsystem +func (n *NetworkEngine) Start() error { + if n.p2pNetwork.Configured() { + // It's possible that the Nuts node isn't bootstrapped (e.g. Node CA certificate missing) but that shouldn't + // prevent it from starting. In that case the NetworkEngine will be in 'offline mode', meaning it can be read from + // and written to, but it will not try to connect to other peers. + if err := n.p2pNetwork.Start(); err != nil { + return err + } + } else { + log.Logger().Warn("NetworkEngine is in offline mode (P2P layer not configured).") + } + n.protocol.Start() + return nil +} + +// Subscribe makes a subscription for the specified document type. The receiver is called when a document +// is received for the specified type. +func (n *NetworkEngine) Subscribe(documentType string, receiver dag.Receiver) { + n.documentGraph.Subscribe(documentType, receiver) +} + +// GetDocument retrieves the document for the given reference. If the document is not known, an error is returned. +func (n *NetworkEngine) GetDocument(documentRef hash.SHA256Hash) (dag.Document, error) { + return n.documentGraph.Get(documentRef) +} + +// GetDocumentPayload retrieves the document payload for the given document. If the document or payload is not found +// nil is returned. +func (n *NetworkEngine) GetDocumentPayload(documentRef hash.SHA256Hash) ([]byte, error) { + document, err := n.documentGraph.Get(documentRef) + if err != nil { + return nil, err + } + return n.payloadStore.ReadPayload(document.Payload()) +} + +// ListDocuments returns all documents known to this NetworkEngine instance. +func (n *NetworkEngine) ListDocuments() ([]dag.Document, error) { + return n.documentGraph.All() +} + +// CreateDocument creates a new document with the specified payload, and signs it using the specified key. +// If the key should be inside the document (instead of being referred to) `attachKey` should be true. +func (n *NetworkEngine) CreateDocument(payloadType string, payload []byte, signingKeyID string, attachKey bool, timestamp time.Time, fieldOpts ...dag.FieldOpt) (dag.Document, error) { + payloadHash := hash.SHA256Sum(payload) + log.Logger().Infof("Creating document (payload hash=%s,type=%s,length=%d,signingKey=%s)", payloadHash, payloadType, len(payload), signingKeyID) + // Create document + prevs := n.documentGraph.Heads() + unsignedDocument, err := dag.NewDocument(payloadHash, payloadType, prevs, fieldOpts...) + if err != nil { + return nil, fmt.Errorf("unable to create new document: %w", err) + } + // Sign it + var document dag.Document + var signer dag.DocumentSigner + if attachKey { + signer = dag.NewAttachedJWKDocumentSigner(n.keyStore, signingKeyID, n.keyStore) + } else { + signer = dag.NewDocumentSigner(n.keyStore, signingKeyID) + } + document, err = signer.Sign(unsignedDocument, timestamp) + if err != nil { + return nil, fmt.Errorf("unable to sign newly created document: %w", err) + } + // Store on local DAG and publish it + if err = n.documentGraph.Add(document); err != nil { + return nil, fmt.Errorf("unable to add newly created document to DAG: %w", err) + } + if err = n.payloadStore.WritePayload(payloadHash, payload); err != nil { + return nil, fmt.Errorf("unable to store payload of newly created document: %w", err) + } + return document, nil +} + +// Shutdown cleans up any leftover go routines +func (n *NetworkEngine) Shutdown() error { + return n.p2pNetwork.Stop() +} + +// Diagnostics collects and returns diagnostics for the NetworkEngine engine. +func (n *NetworkEngine) Diagnostics() []core.DiagnosticResult { + var results = make([]core.DiagnosticResult, 0) + results = append(results, n.protocol.Diagnostics()...) + results = append(results, n.p2pNetwork.Diagnostics()...) + if graph, ok := n.documentGraph.(core.Diagnosable); ok { + results = append(results, graph.Diagnostics()...) + } + return results +} + +func (n *NetworkEngine) buildP2PConfig(peerID p2p.PeerID) (*p2p.P2PNetworkConfig, error) { + cfg := p2p.P2PNetworkConfig{ + ListenAddress: n.Config.GrpcAddr, + PublicAddress: n.Config.PublicAddr, + BootstrapNodes: n.Config.parseBootstrapNodes(), + PeerID: peerID, + } + var err error + if cfg.TrustStore, err = n.Config.loadTrustStore(); err != nil { + return nil, err + } + clientCertificate, err := tls.LoadX509KeyPair(n.Config.CertFile, n.Config.CertKeyFile) + if err != nil { + return nil, errors.Wrapf(err, "unable to load node TLS client certificate (certFile=%s,certKeyFile=%s)", n.Config.CertFile, n.Config.CertKeyFile) + } + cfg.ClientCert = clientCertificate + // Load TLS server certificate, only if enableTLS=true and gRPC server should be started. + if n.Config.GrpcAddr != "" && n.Config.EnableTLS { + cfg.ServerCert = cfg.ClientCert + } + return &cfg, nil +} diff --git a/network/network_integration_test.go b/network/network_integration_test.go new file mode 100644 index 0000000000..ae77f1fd3c --- /dev/null +++ b/network/network_integration_test.go @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package network + +import ( + "crypto" + "fmt" + "github.com/nuts-foundation/nuts-node/core" + nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/network/dag" + "github.com/nuts-foundation/nuts-node/network/log" + "github.com/nuts-foundation/nuts-node/network/p2p" + "github.com/nuts-foundation/nuts-node/network/proto" + "github.com/nuts-foundation/nuts-node/test/io" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "hash/crc32" + "os" + "path" + "sync" + "testing" + "time" +) + +const defaultTimeout = 2 * time.Second +const documentType = "test/document" + +var mutex = sync.Mutex{} +var receivedDocuments = make(map[string][]dag.Document, 0) + +func TestNetwork(t *testing.T) { + testDirectory := io.TestDirectory(t) + expectedDocLogSize := 0 + + // Start 3 nodes: bootstrap, node1 and node2. Node 1 and 2 connect to the bootstrap node and should discover + // each other that way. + bootstrap, err := startNode("bootstrap", path.Join(testDirectory, "bootstrap")) + if !assert.NoError(t, err) { + return + } + node1, err := startNode("node1", path.Join(testDirectory, "node1")) + if !assert.NoError(t, err) { + return + } + node1.p2pNetwork.ConnectToPeer(nameToAddress("bootstrap")) + node2, err := startNode("node2", path.Join(testDirectory, "node2")) + if !assert.NoError(t, err) { + return + } + node2.p2pNetwork.ConnectToPeer(nameToAddress("bootstrap")) + stop := func() { + node2.Shutdown() + node1.Shutdown() + bootstrap.Shutdown() + } + + // Wait until nodes are connected + if !waitFor(t, func() (bool, error) { + return len(node1.p2pNetwork.Peers()) == 1 && len(node2.p2pNetwork.Peers()) == 1, nil + }, defaultTimeout, "time-out while waiting for node 1 and 2 to have 2 peers") { + stop() + return + } + + // Publish first document on node1 and we expect in to come out on node2 and bootstrap + if !addDocumentAndWaitForItToArrive(t, "doc1", node1, "node2", "bootstrap") { + stop() + return + } + expectedDocLogSize++ + + // Now the graph has a root, and node2 can publish a document + if !addDocumentAndWaitForItToArrive(t, "doc2", node2, "node1", "bootstrap") { + stop() + return + } + expectedDocLogSize++ + + // Now assert that all nodes have received all documents + waitForDocuments := func(node string, graph dag.DAG) bool { + return waitFor(t, func() (bool, error) { + if docs, err := graph.All(); err != nil { + return false, err + } else { + return len(docs) == expectedDocLogSize, nil + } + }, defaultTimeout, "%s: time-out while waiting for %d documents", node, expectedDocLogSize) + } + waitForDocuments("bootstrap", bootstrap.documentGraph) + waitForDocuments("node 1", node1.documentGraph) + waitForDocuments("node 2", node2.documentGraph) + + // Can we request the diagnostics? + fmt.Printf("%v\n", bootstrap.Diagnostics()) + fmt.Printf("%v\n", node1.Diagnostics()) + fmt.Printf("%v\n", node2.Diagnostics()) +} + +func addDocumentAndWaitForItToArrive(t *testing.T, payload string, sender *NetworkEngine, receivers ...string) bool { + expectedDocument, err := sender.CreateDocument(documentType, []byte(payload), "key", true, time.Now()) + if !assert.NoError(t, err) { + return true + } + for _, receiver := range receivers { + if !waitFor(t, func() (bool, error) { + mutex.Lock() + defer mutex.Unlock() + for _, receivedDoc := range receivedDocuments[receiver] { + if expectedDocument.Ref().Equals(receivedDoc.Ref()) { + return true, nil + } + } + return false, nil + }, 2*time.Second, "time-out while waiting for document to arrive at %s", receiver) { + return false + } + } + return true +} + +func startNode(name string, directory string) (*NetworkEngine, error) { + log.Logger().Infof("Starting node: %s", name) + os.MkdirAll(directory, os.ModePerm) + logrus.SetLevel(logrus.DebugLevel) + core.NutsConfig().Load(&cobra.Command{}) + mutex.Lock() + mutex.Unlock() + // Initialize crypto instance + cryptoInstance := nutsCrypto.Instance() + cryptoInstance.Config = nutsCrypto.Config{Fspath: directory} + if err := cryptoInstance.Configure(); err != nil { + return nil, err + } + nutsCrypto.Instance().New(func(key crypto.PublicKey) (string, error) { + return "key", nil + }) + // Create NetworkEngine instance + instance := &NetworkEngine{ + p2pNetwork: p2p.NewP2PNetwork(), + protocol: proto.NewProtocol(), + keyStore: cryptoInstance, + Config: Config{ + GrpcAddr: fmt.Sprintf(":%d", nameToPort(name)), + DatabaseFile: path.Join(directory, "network.db"), + PublicAddr: fmt.Sprintf("localhost:%d", nameToPort(name)), + CertFile: "test/certificate-and-key.pem", + CertKeyFile: "test/certificate-and-key.pem", + TrustStoreFile: "test/truststore.pem", + EnableTLS: true, + AdvertHashesInterval: 500, + }, + } + if err := instance.Configure(); err != nil { + return nil, err + } + if err := instance.Start(); err != nil { + return nil, err + } + instance.Subscribe(documentType, func(document dag.Document, payload []byte) error { + mutex.Lock() + defer mutex.Unlock() + println("document", string(payload), "arrived at", name) + receivedDocuments[name] = append(receivedDocuments[name], document) + return nil + }) + return instance, nil +} + +func nameToPort(name string) int { + return int(crc32.ChecksumIEEE([]byte(name))%9000 + 1000) +} + +func nameToAddress(name string) string { + return fmt.Sprintf("localhost:%d", nameToPort(name)) +} + +type predicate func() (bool, error) + +func waitFor(t *testing.T, p predicate, timeout time.Duration, message string, msgArgs ...interface{}) bool { + deadline := time.Now().Add(timeout) + for { + b, err := p() + if !assert.NoError(t, err) { + return false + } + if b { + return true + } + if time.Now().After(deadline) { + assert.Fail(t, fmt.Sprintf(message, msgArgs...)) + return false + } + time.Sleep(100 * time.Millisecond) + } +} diff --git a/network/network_test.go b/network/network_test.go new file mode 100644 index 0000000000..f03574d21b --- /dev/null +++ b/network/network_test.go @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package network + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "github.com/golang/mock/gomock" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + "github.com/nuts-foundation/nuts-node/network/p2p" + "github.com/nuts-foundation/nuts-node/network/proto" + "github.com/nuts-foundation/nuts-node/test/io" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +type networkTestContext struct { + network *NetworkEngine + p2pNetwork *p2p.MockP2PNetwork + protocol *proto.MockProtocol + graph *dag.MockDAG + payload *dag.MockPayloadStore + keyStore *crypto.MockKeyStore +} + +func TestNetwork_ListDocuments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.graph.EXPECT().All().Return([]dag.Document{dag.CreateTestDocument(1)}, nil) + docs, err := cxt.network.ListDocuments() + assert.Len(t, docs, 1) + assert.NoError(t, err) + }) +} + +func TestNetwork_GetDocument(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.graph.EXPECT().Get(gomock.Any()) + cxt.network.GetDocument(hash.EmptyHash()) + }) +} + +func TestNetwork_GetDocumentContents(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + document := dag.CreateTestDocument(1) + cxt.graph.EXPECT().Get(document.Ref()).Return(document, nil) + cxt.payload.EXPECT().ReadPayload(document.Payload()) + cxt.network.GetDocumentPayload(document.Ref()) + }) +} + +func TestNetwork_Subscribe(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.graph.EXPECT().Subscribe("some-type", nil) + cxt.network.Subscribe("some-type", nil) + }) +} + +func TestNetwork_Diagnostics(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.protocol.EXPECT().Diagnostics().Return([]core.DiagnosticResult{stat{}, stat{}}) + cxt.p2pNetwork.EXPECT().Diagnostics().Return([]core.DiagnosticResult{stat{}, stat{}}) + diagnostics := cxt.network.Diagnostics() + assert.Len(t, diagnostics, 4) + }) +} + +func TestNetwork_Configure(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.protocol.EXPECT().Configure(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + cxt.p2pNetwork.EXPECT().Configure(gomock.Any()) + err := cxt.network.Configure() + if !assert.NoError(t, err) { + return + } + }) +} + +func TestNetwork_CreateDocument(t *testing.T) { + privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + t.Run("attach key", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + payload := []byte("Hello, World!") + cxt := createNetwork(t, ctrl) + cxt.p2pNetwork.EXPECT().Start() + cxt.p2pNetwork.EXPECT().Configured().Return(true) + cxt.protocol.EXPECT().Start() + cxt.graph.EXPECT().Heads().Return(nil) + cxt.graph.EXPECT().Add(gomock.Any()) + cxt.payload.EXPECT().WritePayload(hash.SHA256Sum(payload), payload) + cxt.keyStore.EXPECT().GetPublicKey("signing-key").Return(privateKey.PublicKey, nil) + cxt.keyStore.EXPECT().SignJWS(gomock.Any(), gomock.Any(), gomock.Eq("signing-key")) + err := cxt.network.Start() + if !assert.NoError(t, err) { + return + } + _, err = cxt.network.CreateDocument(documentType, payload, "signing-key", true, time.Now()) + assert.NoError(t, err) + }) + t.Run("detached key", func(t *testing.T) { + payload := []byte("Hello, World!") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cxt := createNetwork(t, ctrl) + cxt.p2pNetwork.EXPECT().Start() + cxt.p2pNetwork.EXPECT().Configured().Return(true) + cxt.protocol.EXPECT().Start() + cxt.graph.EXPECT().Heads().Return(nil) + cxt.graph.EXPECT().Add(gomock.Any()) + cxt.payload.EXPECT().WritePayload(hash.SHA256Sum(payload), payload) + cxt.keyStore.EXPECT().SignJWS(gomock.Any(), gomock.Any(), gomock.Eq("signing-key")) + err := cxt.network.Start() + if !assert.NoError(t, err) { + return + } + _, err = cxt.network.CreateDocument(documentType, payload, "signing-key", false, time.Now()) + assert.NoError(t, err) + }) +} + +func TestNetwork_Start(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.p2pNetwork.EXPECT().Start() + cxt.p2pNetwork.EXPECT().Configured().Return(true) + cxt.protocol.EXPECT().Start() + err := cxt.network.Start() + if !assert.NoError(t, err) { + return + } + }) + t.Run("ok - NetworkEngine offline", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.p2pNetwork.EXPECT().Configured().Return(false) + cxt.protocol.EXPECT().Start() + err := cxt.network.Start() + if !assert.NoError(t, err) { + return + } + }) +} + +func TestNetwork_Shutdown(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.p2pNetwork.EXPECT().Stop() + err := cxt.network.Shutdown() + assert.NoError(t, err) + }) + t.Run("error - stop returns error", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.p2pNetwork.EXPECT().Stop().Return(errors.New("failed")) + err := cxt.network.Shutdown() + assert.EqualError(t, err, "failed") + }) +} + +func TestNetwork_buildP2PNetworkConfig(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + t.Run("ok - TLS enabled", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.network.Config.GrpcAddr = ":5555" + cxt.network.Config.EnableTLS = true + cxt.network.Config.CertFile = "test/certificate-and-key.pem" + cxt.network.Config.CertKeyFile = "test/certificate-and-key.pem" + cfg, err := cxt.network.buildP2PConfig("") + assert.NotNil(t, cfg) + assert.NoError(t, err) + assert.NotNil(t, cfg.ClientCert.PrivateKey) + assert.NotNil(t, cfg.ServerCert.PrivateKey) + }) + t.Run("ok - TLS disabled", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.network.Config.GrpcAddr = ":5555" + cxt.network.Config.EnableTLS = false + cfg, err := cxt.network.buildP2PConfig("") + assert.NotNil(t, cfg) + assert.NoError(t, err) + assert.NotNil(t, cfg.ClientCert.PrivateKey) + assert.Nil(t, cfg.ServerCert.PrivateKey) + }) + t.Run("ok - gRPC server not bound", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.network.Config.GrpcAddr = "" + cxt.network.Config.EnableTLS = true + cfg, err := cxt.network.buildP2PConfig("") + assert.NotNil(t, cfg) + assert.NoError(t, err) + assert.NotNil(t, cfg.ClientCert.PrivateKey) + assert.Nil(t, cfg.ServerCert.PrivateKey) + }) + t.Run("error - unable to load key pair from file", func(t *testing.T) { + cxt := createNetwork(t, ctrl) + cxt.network.Config.CertFile = "test/non-existent.pem" + cxt.network.Config.CertKeyFile = "test/non-existent.pem" + cxt.network.Config.EnableTLS = true + cfg, err := cxt.network.buildP2PConfig("") + assert.Nil(t, cfg) + assert.EqualError(t, err, "unable to load node TLS client certificate (certFile=test/non-existent.pem,certKeyFile=test/non-existent.pem): open test/non-existent.pem: no such file or directory") + }) +} + +func createNetwork(t *testing.T, ctrl *gomock.Controller) *networkTestContext { + p2pNetwork := p2p.NewMockP2PNetwork(ctrl) + protocol := proto.NewMockProtocol(ctrl) + graph := dag.NewMockDAG(ctrl) + payload := dag.NewMockPayloadStore(ctrl) + testDirectory := io.TestDirectory(t) + networkConfig := TestNetworkConfig(testDirectory) + networkConfig.TrustStoreFile = "test/truststore.pem" + networkConfig.CertFile = "test/certificate-and-key.pem" + networkConfig.CertKeyFile = "test/certificate-and-key.pem" + networkConfig.PublicAddr = "foo:8080" + keyStore := crypto.NewMockKeyStore(ctrl) + network := NewNetworkInstance(networkConfig, keyStore) + network.p2pNetwork = p2pNetwork + network.protocol = protocol + network.documentGraph = graph + network.payloadStore = payload + return &networkTestContext{ + network: network, + p2pNetwork: p2pNetwork, + protocol: protocol, + graph: graph, + payload: payload, + keyStore: keyStore, + } +} + +type stat struct { +} + +func (s stat) Name() string { + return "key" +} + +func (s stat) String() string { + return "value" +} diff --git a/network/p2p/backoff.go b/network/p2p/backoff.go new file mode 100644 index 0000000000..af70aa6114 --- /dev/null +++ b/network/p2p/backoff.go @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "time" +) + +type Backoff interface { + Reset() + Backoff() time.Duration +} + +type backoff struct { + multiplier float64 + value time.Duration + max time.Duration + min time.Duration +} + +func (b *backoff) Reset() { + b.value = 0 +} + +func (b *backoff) Backoff() time.Duration { + // TODO: Might want to add jitter (e.g. https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md) + if b.value < b.min { + b.value = b.min + } else { + b.value = time.Duration(float64(b.value) * b.multiplier) + if b.value > b.max { + b.value = b.max + } + } + return b.value +} + +func defaultBackoff() Backoff { + // TODO: Make this configurable + return &backoff{ + multiplier: 1.5, + value: 0, + max: 30 * time.Second, + min: time.Second, + } +} diff --git a/network/p2p/backoff_test.go b/network/p2p/backoff_test.go new file mode 100644 index 0000000000..fd15120c6c --- /dev/null +++ b/network/p2p/backoff_test.go @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestBackoff(t *testing.T) { + b := defaultBackoff().(*backoff) + // Initially the backoff should be min-backoff + assert.Equal(t, b.min, b.Backoff()) + var i = 0 + for i = 0; i < 10; i++ { + b.Backoff().Milliseconds() + } + // In a few passes we should have reached max-backoff + assert.Equal(t, b.max, b.Backoff()) +} + +func TestBackoffReset(t *testing.T) { + b := defaultBackoff().(*backoff) + b.Backoff() + assert.True(t, b.Backoff() > b.min) + b.Reset() + assert.Equal(t, b.min, b.Backoff()) +} + +func TestBackoffDefaultValues(t *testing.T) { + b := defaultBackoff().(*backoff) + assert.Equal(t, time.Second, b.min) + assert.Equal(t, 30*time.Second, b.max) +} diff --git a/network/p2p/impl-peer.go b/network/p2p/impl-peer.go new file mode 100644 index 0000000000..aab213f1bc --- /dev/null +++ b/network/p2p/impl-peer.go @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "fmt" + log "github.com/nuts-foundation/nuts-node/network/log" + "github.com/nuts-foundation/nuts-node/network/transport" + "google.golang.org/grpc" + "io" + "sync" +) + +type messageGate interface { + Send(message *transport.NetworkMessage) error + Recv() (*transport.NetworkMessage, error) +} + +// Peer represents a connected peer +type peer struct { + id PeerID + // address holds the remote address of the node we're actually connected to + address string + // gate is used to send and receive messages + gate messageGate + // conn and client are only filled for peers where we're the connecting party + conn *grpc.ClientConn + client transport.NetworkClient + // outMessages contains the messages we want to send to the peer. + // According to the docs it's unsafe to simultaneously call stream.Send() from multiple goroutines so we put them + // on a channel so that each peer can have its own goroutine sending messages (consuming messages from this channel) + outMessages chan *transport.NetworkMessage + // closeMutex the close() function since race conditions can trigger panics + closeMutex *sync.Mutex +} + +func (p peer) String() string { + return fmt.Sprintf("%s@%s", p.id, p.address) +} + +func (p *peer) close() { + p.closeMutex.Lock() + defer p.closeMutex.Unlock() + if p.conn != nil { + if err := p.conn.Close(); err != nil { + log.Logger().Errorf("Unable to close client connection (peer=%s): %v", p, err) + } + p.conn = nil + } + if p.outMessages != nil { + close(p.outMessages) + p.outMessages = nil + } +} + +func (p *peer) send(message *transport.NetworkMessage) { + p.closeMutex.Lock() + defer p.closeMutex.Unlock() + p.outMessages <- message +} + +// sendMessages (blocking) reads messages from its outMessages channel and sends them to the peer until the channel is closed. +func (p peer) sendMessages() { + for message := range p.outMessages { + if p.gate.Send(message) != nil { + log.Logger().Errorf("Unable to broadcast message to peer (peer=%s)", p.id) + } + } +} + +// receiveMessages (blocking) reads messages from the peer until it disconnects or the network is stopped. +func receiveMessages(gate messageGate, peerId PeerID, receivedMsgQueue messageQueue) { + for { + msg, recvErr := gate.Recv() + if recvErr != nil { + if recvErr == io.EOF { + log.Logger().Infof("Peer closed connection: %s", peerId) + } else { + log.Logger().Errorf("Peer connection error (peer=%s): %v", peerId, recvErr) + } + break + } + log.Logger().Tracef("Received message from peer (%s): %s", peerId, msg.String()) + receivedMsgQueue.c <- PeerMessage{ + Peer: peerId, + Message: msg, + } + } +} diff --git a/network/p2p/impl-util.go b/network/p2p/impl-util.go new file mode 100644 index 0000000000..7a470d025d --- /dev/null +++ b/network/p2p/impl-util.go @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "fmt" + "google.golang.org/grpc/metadata" + "net" + "strings" +) + +func normalizeAddress(addr string) string { + var normalizedAddr string + host, port, err := net.SplitHostPort(addr) + if err != nil { + normalizedAddr = addr + } else { + if host == "localhost" { + host = "127.0.0.1" + normalizedAddr = net.JoinHostPort(host, port) + } else { + normalizedAddr = addr + } + } + return normalizedAddr +} + +const peerIDHeader = "peerID" + +func peerIDFromMetadata(md metadata.MD) (PeerID, error) { + values := md.Get(peerIDHeader) + if len(values) == 0 { + return "", fmt.Errorf("peer didn't send %s header", peerIDHeader) + } else if len(values) > 1 { + return "", fmt.Errorf("peer sent multiple values for %s header", peerIDHeader) + } + peerID := PeerID(strings.TrimSpace(values[0])) + if peerID == "" { + return "", fmt.Errorf("peer sent empty %s header", peerIDHeader) + } + return peerID, nil +} + +func constructMetadata(peerID PeerID) metadata.MD { + return metadata.New(map[string]string{peerIDHeader: string(peerID)}) +} diff --git a/network/p2p/impl-util_test.go b/network/p2p/impl-util_test.go new file mode 100644 index 0000000000..a2aefbae0e --- /dev/null +++ b/network/p2p/impl-util_test.go @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/metadata" + "testing" +) + +func Test_normalizeAddress(t *testing.T) { + t.Run("invalid address", func(t *testing.T) { + assert.Equal(t, "not a valid address", normalizeAddress("not a valid address")) + }) + t.Run("address is already normalized (IP)", func(t *testing.T) { + assert.Equal(t, "1.2.3.4:1234", normalizeAddress("1.2.3.4:1234")) + }) + t.Run("address is already normalized (hostname)", func(t *testing.T) { + assert.Equal(t, "foobar:1234", normalizeAddress("foobar:1234")) + }) + t.Run("address is localhost (hostname)", func(t *testing.T) { + assert.Equal(t, "127.0.0.1:1234", normalizeAddress("localhost:1234")) + }) + t.Run("address is localhost (IP)", func(t *testing.T) { + assert.Equal(t, "127.0.0.1:1234", normalizeAddress("127.0.0.1:1234")) + }) +} + +func Test_peerIDFromMetadata(t *testing.T) { + t.Run("ok - roundtrip", func(t *testing.T) { + md := constructMetadata("1234") + peerID, err := peerIDFromMetadata(md) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "1234", peerID.String()) + }) + t.Run("error - multiple values", func(t *testing.T) { + md := metadata.MD{} + md.Append(peerIDHeader, "1") + md.Append(peerIDHeader, "2") + peerID, err := peerIDFromMetadata(md) + assert.EqualError(t, err, "peer sent multiple values for peerID header") + assert.Empty(t, peerID.String()) + }) + t.Run("error - no values", func(t *testing.T) { + md := metadata.MD{} + peerID, err := peerIDFromMetadata(md) + assert.EqualError(t, err, "peer didn't send peerID header") + assert.Empty(t, peerID.String()) + }) + t.Run("error - empty value", func(t *testing.T) { + md := metadata.MD{} + md.Set(peerIDHeader, " ") + peerID, err := peerIDFromMetadata(md) + assert.EqualError(t, err, "peer sent empty peerID header") + assert.Empty(t, peerID.String()) + }) +} diff --git a/network/p2p/impl.go b/network/p2p/impl.go new file mode 100644 index 0000000000..d700caac54 --- /dev/null +++ b/network/p2p/impl.go @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/network/log" + "github.com/nuts-foundation/nuts-node/network/transport" + errors2 "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + grpcPeer "google.golang.org/grpc/peer" + "net" + "strings" + "sync" + "time" +) + +type Dialer func(ctx context.Context, target string, opts ...grpc.DialOption) (conn *grpc.ClientConn, err error) + +const connectingQueueChannelSize = 100 + +type p2pNetwork struct { + config P2PNetworkConfig + + grpcServer *grpc.Server + listener net.Listener + + // connectors contains the list of peers we're currently trying to connect to. + connectors map[string]*connector + // connectorAddChannel is used to communicate addresses of remote peers to connect to. + connectorAddChannel chan string + // peers is the list of peers we're actually connected to. Access MUST be wrapped in locking using peerReadLock and peerWriteLock. + peers map[PeerID]*peer + // peersByAddr access MUST be wrapped in locking using peerReadLock and peerWriteLock. + peersByAddr map[string]PeerID + peerMutex *sync.Mutex + receivedMessages messageQueue + peerDialer Dialer + configured bool +} + +func (n p2pNetwork) Configured() bool { + return n.configured +} + +func (n p2pNetwork) Diagnostics() []core.DiagnosticResult { + peers := n.Peers() + return []core.DiagnosticResult{ + NumberOfPeersStatistic{NumberOfPeers: len(peers)}, + PeersStatistic{Peers: peers}, + } +} + +func (n *p2pNetwork) Peers() []PeerID { + var result []PeerID + n.peerMutex.Lock() + defer n.peerMutex.Unlock() + for _, peer := range n.peers { + result = append(result, peer.id) + } + return result +} + +func (n *p2pNetwork) Broadcast(message *transport.NetworkMessage) { + n.peerMutex.Lock() + defer n.peerMutex.Unlock() + for _, peer := range n.peers { + peer.outMessages <- message + } +} + +func (n p2pNetwork) ReceivedMessages() MessageQueue { + return n.receivedMessages +} + +func (n p2pNetwork) Send(peerId PeerID, message *transport.NetworkMessage) error { + // TODO: Can't we optimize this so that we don't need this lock? Maybe by (secretly) embedding a pointer to the peer in the peer ID? + var peer *peer + n.peerMutex.Lock() + { + defer n.peerMutex.Unlock() + peer = n.peers[peerId] + } + if peer == nil { + return fmt.Errorf("unknown peer: %s", peerId) + } + peer.outMessages <- message + return nil +} + +type connector struct { + address string + backoff Backoff + Dialer +} + +func (c *connector) connect(ownID PeerID, config *tls.Config) (*peer, error) { + log.Logger().Infof("Connecting to peer: %v", c.address) + cxt := metadata.NewOutgoingContext(context.Background(), constructMetadata(ownID)) + dialContext, _ := context.WithTimeout(cxt, 5*time.Second) + conn, err := c.Dialer(dialContext, c.address, + grpc.WithBlock(), // Dial should block until connection succeeded (or time-out expired) + grpc.WithTransportCredentials(credentials.NewTLS(config)), // TLS authentication + grpc.WithReturnConnectionError()) // This option causes underlying errors to be returned when connections fail, rather than just "context deadline exceeded" + if err != nil { + return nil, errors2.Wrap(err, "unable to connect") + } + client := transport.NewNetworkClient(conn) + gate, err := client.Connect(cxt) + if err != nil { + log.Logger().Errorf("Failed to set up stream (peer=%s): %v", c.address, err) + _ = conn.Close() + return nil, err + } + + peer := peer{ + conn: conn, + client: client, + gate: gate, + address: c.address, + closeMutex: &sync.Mutex{}, + } + if serverHeader, err := gate.Header(); err != nil { + log.Logger().Errorf("Error receiving headers from server (peer=%s): %v", c.address, err) + _ = conn.Close() + return nil, err + } else { + if serverPeerID, err := peerIDFromMetadata(serverHeader); err != nil { + log.Logger().Errorf("Error parsing PeerID header from server (peer=%s): %v", c.address, err) + _ = conn.Close() + return nil, err + } else if serverPeerID == "" { + log.Logger().Warnf("Server didn't send a peer ID, dropping connection (peer=%s)", c.address) + _ = conn.Close() + return nil, err + } else { + peer.id = serverPeerID + } + } + + return &peer, nil +} + +func NewP2PNetwork() P2PNetwork { + return &p2pNetwork{ + peers: make(map[PeerID]*peer, 0), + peersByAddr: make(map[string]PeerID, 0), + connectors: make(map[string]*connector, 0), + connectorAddChannel: make(chan string, connectingQueueChannelSize), // TODO: Does this number make sense? + peerMutex: &sync.Mutex{}, + receivedMessages: messageQueue{c: make(chan PeerMessage, 100)}, // TODO: Does this number make sense? + peerDialer: grpc.DialContext, + } +} + +func NewP2PNetworkWithOptions(listener net.Listener, dialer Dialer) P2PNetwork { + result := NewP2PNetwork().(*p2pNetwork) + result.listener = listener + result.peerDialer = dialer + return result +} + +type messageQueue struct { + c chan PeerMessage +} + +func (m messageQueue) Get() PeerMessage { + return <-m.c +} + +func (n *p2pNetwork) Configure(config P2PNetworkConfig) error { + if config.PeerID == "" { + return errors.New("PeerID is empty") + } + if config.TrustStore == nil { + return errors.New("TrustStore is nil") + } + n.config = config + n.configured = true + for _, bootstrapNode := range n.config.BootstrapNodes { + n.ConnectToPeer(bootstrapNode) + } + return nil +} + +func (n *p2pNetwork) Start() error { + log.Logger().Infof("Starting gRPC P2P node (ID: %s)", n.config.PeerID) + if n.config.ListenAddress != "" { + log.Logger().Infof("Starting gRPC server on %s", n.config.ListenAddress) + var err error + // We allow test code to set the listener to allow for in-memory (bufnet) channels + var serverOpts = make([]grpc.ServerOption, 0) + if n.listener == nil { + n.listener, err = net.Listen("tcp", n.config.ListenAddress) + if err != nil { + return err + } + if n.config.ServerCert.PrivateKey == nil { + log.Logger().Info("TLS is disabled on gRPC server side! Make sure SSL/TLS offloading is properly configured.") + } else { + serverOpts = append(serverOpts, grpc.Creds(credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{n.config.ServerCert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: n.config.TrustStore, + }))) + } + } + n.grpcServer = grpc.NewServer(serverOpts...) + transport.RegisterNetworkServer(n.grpcServer, n) + go func() { + err = n.grpcServer.Serve(n.listener) + if err != nil && !errors.Is(err, grpc.ErrServerStopped) { + log.Logger().Errorf("gRPC server errored: %v", err) + _ = n.Stop() + } + }() + } + // Start client + go n.connectToNewPeers() + return nil +} + +func (n *p2pNetwork) Stop() error { + // Stop server + if n.grpcServer != nil { + n.grpcServer.Stop() + n.grpcServer = nil + } + if n.listener != nil { + if err := n.listener.Close(); err != nil { + log.Logger().Warn("Error while closing server listener: ", err) + } + n.listener = nil + } + close(n.connectorAddChannel) + // Stop client + n.peerMutex.Lock() + defer n.peerMutex.Unlock() + for _, peer := range n.peers { + peer.close() + } + return nil +} + +func (n p2pNetwork) ConnectToPeer(address string) bool { + if n.shouldConnectTo(address) && len(n.connectorAddChannel) < connectingQueueChannelSize { + n.connectorAddChannel <- address + return true + } + return false +} + +func (n *p2pNetwork) sendAndReceiveForPeer(peer *peer) { + peer.outMessages = make(chan *transport.NetworkMessage, 10) // TODO: Does this number make sense? Should also be configurable? + go peer.sendMessages() + n.addPeer(peer) + // TODO: Check PeerID sent by peer + receiveMessages(peer.gate, peer.id, n.receivedMessages) + peer.close() + // When we reach this line, receiveMessages has exited which means the connection has been closed. + n.removePeer(peer) +} + +// connectToNewPeers reads from connectorAddChannel to start connecting to new peers +func (n *p2pNetwork) connectToNewPeers() { + for address := range n.connectorAddChannel { + if _, present := n.peersByAddr[address]; present { + log.Logger().Infof("Not connecting to peer, already connected (address=%s)", address) + } else if n.connectors[address] != nil { + log.Logger().Infof("Not connecting to peer, already trying to connect (address=%s)", address) + } else { + newConnector := &connector{ + address: address, + backoff: defaultBackoff(), + Dialer: n.peerDialer, + } + n.connectors[address] = newConnector + log.Logger().Infof("Added remote peer (address=%s)", address) + go func() { + for { + if n.shouldConnectTo(address) { + tlsConfig := tls.Config{ + Certificates: []tls.Certificate{n.config.ClientCert}, + RootCAs: n.config.TrustStore, + } + if peer, err := newConnector.connect(n.config.PeerID, &tlsConfig); err != nil { + waitPeriod := newConnector.backoff.Backoff() + log.Logger().Warnf("Couldn't connect to peer, reconnecting in %d seconds (peer=%s,err=%v)", int(waitPeriod.Seconds()), newConnector.address, err) + time.Sleep(waitPeriod) + } else { + n.sendAndReceiveForPeer(peer) + newConnector.backoff.Reset() + log.Logger().Infof("Connected to peer (address=%s)", newConnector.address) + } + } + time.Sleep(5 * time.Second) + } + }() + } + } +} + +// shouldConnectTo checks whether we should connect to the given node. +func (n p2pNetwork) shouldConnectTo(address string) bool { + normalizedAddress := normalizeAddress(address) + if normalizedAddress == normalizeAddress(n.getLocalAddress()) { + // We're not going to connect to our own node + log.Logger().Debug("Not connecting since it's localhost") + return false + } + var result = true + n.peerMutex.Lock() + defer n.peerMutex.Unlock() + if _, present := n.peersByAddr[normalizedAddress]; present { + // We're not going to connect to a node we're already connected to + log.Logger().Tracef("Not connecting since we're already connected (address=%s)", normalizedAddress) + result = false + } + return result +} + +func (n p2pNetwork) getLocalAddress() string { + if n.config.PublicAddress != "" { + return n.config.PublicAddress + } else { + if strings.HasPrefix(n.config.ListenAddress, ":") { + // Interface's address not included in listening address (e.g. :5555), so prepend with localhost + return "localhost" + n.config.ListenAddress + } else { + // Interface's address included in listening address (e.g. localhost:5555), so return as-is. + return n.config.ListenAddress + } + } +} + +func (n p2pNetwork) isRunning() bool { + return n.grpcServer != nil +} + +func (n p2pNetwork) Connect(stream transport.Network_ConnectServer) error { + peerCtx, _ := grpcPeer.FromContext(stream.Context()) + log.Logger().Infof("New peer connected from %s", peerCtx.Addr) + md, ok := metadata.FromIncomingContext(stream.Context()) + if !ok { + return errors.New("unable to get metadata") + } + peerID, err := peerIDFromMetadata(md) + if err != nil { + return err + } + // We received our peer's PeerID, now send our own. + if err := stream.SendHeader(constructMetadata(n.config.PeerID)); err != nil { + return errors2.Wrap(err, "unable to send headers") + } + peer := &peer{ + id: peerID, + gate: stream, + address: peerCtx.Addr.String(), + closeMutex: &sync.Mutex{}, + } + n.sendAndReceiveForPeer(peer) + return nil +} + +func (n *p2pNetwork) addPeer(peer *peer) { + n.peerMutex.Lock() + defer n.peerMutex.Unlock() + + n.peers[peer.id] = peer + n.peersByAddr[normalizeAddress(peer.address)] = peer.id +} + +func (n *p2pNetwork) removePeer(peer *peer) { + n.peerMutex.Lock() + defer n.peerMutex.Unlock() + + peer = n.peers[peer.id] + if peer == nil { + return + } + + delete(n.peers, peer.id) + delete(n.peersByAddr, normalizeAddress(peer.address)) +} diff --git a/network/p2p/impl_test.go b/network/p2p/impl_test.go new file mode 100644 index 0000000000..f4cf6ce0d9 --- /dev/null +++ b/network/p2p/impl_test.go @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "crypto/tls" + "crypto/x509" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func Test_p2pNetwork_Configure(t *testing.T) { + t.Run("ok - configure registers bootstrap nodes", func(t *testing.T) { + network := NewP2PNetwork() + err := network.Configure(P2PNetworkConfig{ + PeerID: "foo", + ListenAddress: "0.0.0.0:555", + BootstrapNodes: []string{"foo:555", "bar:5554"}, + TrustStore: x509.NewCertPool(), + }) + if !assert.NoError(t, err) { + return + } + assert.Len(t, network.(*p2pNetwork).connectorAddChannel, 2) + }) +} + +func Test_p2pNetwork_Start(t *testing.T) { + waitForGRPCStart := func() { + time.Sleep(100 * time.Millisecond) // Wait a moment for gRPC server setup goroutines to run + } + t.Run("ok - gRPC server not bound", func(t *testing.T) { + network := NewP2PNetwork().(*p2pNetwork) + err := network.Configure(P2PNetworkConfig{ + PeerID: "foo", + TrustStore: x509.NewCertPool(), + }) + if !assert.NoError(t, err) { + return + } + err = network.Start() + waitForGRPCStart() + assert.Nil(t, network.listener) + defer network.Stop() + if !assert.NoError(t, err) { + return + } + }) + t.Run("ok - gRPC server bound, TLS enabled", func(t *testing.T) { + network := NewP2PNetwork().(*p2pNetwork) + serverCert, _ := tls.LoadX509KeyPair("../../test/certificate-and-key.pem", "../../test/certificate-and-key.pem") + err := network.Configure(P2PNetworkConfig{ + PeerID: "foo", + ServerCert: serverCert, + ListenAddress: ":5555", + TrustStore: x509.NewCertPool(), + }) + if !assert.NoError(t, err) { + return + } + err = network.Start() + waitForGRPCStart() + assert.NotNil(t, network.listener) + defer network.Stop() + if !assert.NoError(t, err) { + return + } + }) + t.Run("ok - gRPC server bound, TLS disabled", func(t *testing.T) { + network := NewP2PNetwork().(*p2pNetwork) + err := network.Configure(P2PNetworkConfig{ + PeerID: "foo", + ListenAddress: ":5555", + TrustStore: x509.NewCertPool(), + }) + if !assert.NoError(t, err) { + return + } + err = network.Start() + waitForGRPCStart() + assert.NotNil(t, network.listener) + defer network.Stop() + if !assert.NoError(t, err) { + return + } + }) +} + +func Test_p2pNetwork_GetLocalAddress(t *testing.T) { + network := NewP2PNetwork().(*p2pNetwork) + err := network.Configure(P2PNetworkConfig{ + PeerID: "foo", + ListenAddress: "0.0.0.0:555", + BootstrapNodes: []string{"foo:555", "bar:5554"}, + TrustStore: x509.NewCertPool(), + }) + if !assert.NoError(t, err) { + return + } + t.Run("ok - public address not configured, listen address fully qualified", func(t *testing.T) { + assert.Equal(t, "0.0.0.0:555", network.getLocalAddress()) + }) + t.Run("ok - public address not configured, listen address contains only port", func(t *testing.T) { + network.config.ListenAddress = ":555" + assert.Equal(t, "localhost:555", network.getLocalAddress()) + }) + t.Run("ok - public address configured", func(t *testing.T) { + network.config.PublicAddress = "test:1234" + assert.Equal(t, "test:1234", network.getLocalAddress()) + }) +} diff --git a/network/p2p/interface.go b/network/p2p/interface.go new file mode 100644 index 0000000000..977175f793 --- /dev/null +++ b/network/p2p/interface.go @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "crypto/tls" + "crypto/x509" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/network/transport" +) + +// P2PNetwork defines the API for the P2P layer, used to connect to peers and exchange messages. +type P2PNetwork interface { + core.Diagnosable + // Configure configures the P2PNetwork. Must be called before Start(). + Configure(config P2PNetworkConfig) error + // Configured returns whether the system is configured or not + Configured() bool + // Start starts the P2P network on the local node. + Start() error + // Stop stops the P2P network on the local node. + Stop() error + // AddRemoteNode adds a remote node to the local node's view of the network, so it can become one of our peers. + // If we'll try to connect to the remote node, true is returned, otherwise false. + ConnectToPeer(address string) bool + // ReceivedMessages returns a queue containing all messages received from our peers. It must be drained, because when its buffer is full the producer (P2PNetwork) is blocked. + ReceivedMessages() MessageQueue + // Send sends a message to a specific peer. + Send(peer PeerID, message *transport.NetworkMessage) error + // Broadcast sends a message to all peers. + Broadcast(message *transport.NetworkMessage) + // Peers returns our peers (remote nodes we're currently connected to). + Peers() []PeerID +} + +type MessageQueue interface { + Get() PeerMessage +} + +type PeerID string + +func (p PeerID) String() string { + return string(p) +} + +type PeerMessage struct { + Peer PeerID + Message *transport.NetworkMessage +} + +type P2PNetworkConfig struct { + PeerID PeerID + PublicAddress string + // ListenAddress specifies the socket address the gRPC server should listen on. + // If not set, the node will not accept incoming connections (but outbound connections can still be made). + ListenAddress string + BootstrapNodes []string + ClientCert tls.Certificate + // ServerCert specifies the TLS server certificate. If set the server should open a TLS socket, otherwise plain TCP. + ServerCert tls.Certificate + TrustStore *x509.CertPool +} diff --git a/network/p2p/mock.go b/network/p2p/mock.go new file mode 100644 index 0000000000..0cfc8d8b9b --- /dev/null +++ b/network/p2p/mock.go @@ -0,0 +1,210 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: network/p2p/interface.go + +// Package p2p is a generated GoMock package. +package p2p + +import ( + gomock "github.com/golang/mock/gomock" + core "github.com/nuts-foundation/nuts-node/core" + transport "github.com/nuts-foundation/nuts-node/network/transport" + reflect "reflect" +) + +// MockP2PNetwork is a mock of P2PNetwork interface +type MockP2PNetwork struct { + ctrl *gomock.Controller + recorder *MockP2PNetworkMockRecorder +} + +// MockP2PNetworkMockRecorder is the mock recorder for MockP2PNetwork +type MockP2PNetworkMockRecorder struct { + mock *MockP2PNetwork +} + +// NewMockP2PNetwork creates a new mock instance +func NewMockP2PNetwork(ctrl *gomock.Controller) *MockP2PNetwork { + mock := &MockP2PNetwork{ctrl: ctrl} + mock.recorder = &MockP2PNetworkMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockP2PNetwork) EXPECT() *MockP2PNetworkMockRecorder { + return m.recorder +} + +// Diagnostics mocks base method +func (m *MockP2PNetwork) Diagnostics() []core.DiagnosticResult { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Diagnostics") + ret0, _ := ret[0].([]core.DiagnosticResult) + return ret0 +} + +// Diagnostics indicates an expected call of Diagnostics +func (mr *MockP2PNetworkMockRecorder) Diagnostics() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Diagnostics", reflect.TypeOf((*MockP2PNetwork)(nil).Diagnostics)) +} + +// Configure mocks base method +func (m *MockP2PNetwork) Configure(config P2PNetworkConfig) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Configure", config) + ret0, _ := ret[0].(error) + return ret0 +} + +// Configure indicates an expected call of Configure +func (mr *MockP2PNetworkMockRecorder) Configure(config interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configure", reflect.TypeOf((*MockP2PNetwork)(nil).Configure), config) +} + +// Configured mocks base method +func (m *MockP2PNetwork) Configured() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Configured") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Configured indicates an expected call of Configured +func (mr *MockP2PNetworkMockRecorder) Configured() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configured", reflect.TypeOf((*MockP2PNetwork)(nil).Configured)) +} + +// Start mocks base method +func (m *MockP2PNetwork) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *MockP2PNetworkMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockP2PNetwork)(nil).Start)) +} + +// Stop mocks base method +func (m *MockP2PNetwork) Stop() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stop") + ret0, _ := ret[0].(error) + return ret0 +} + +// Stop indicates an expected call of Stop +func (mr *MockP2PNetworkMockRecorder) Stop() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockP2PNetwork)(nil).Stop)) +} + +// ConnectToPeer mocks base method +func (m *MockP2PNetwork) ConnectToPeer(address string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnectToPeer", address) + ret0, _ := ret[0].(bool) + return ret0 +} + +// ConnectToPeer indicates an expected call of ConnectToPeer +func (mr *MockP2PNetworkMockRecorder) ConnectToPeer(address interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectToPeer", reflect.TypeOf((*MockP2PNetwork)(nil).ConnectToPeer), address) +} + +// ReceivedMessages mocks base method +func (m *MockP2PNetwork) ReceivedMessages() MessageQueue { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReceivedMessages") + ret0, _ := ret[0].(MessageQueue) + return ret0 +} + +// ReceivedMessages indicates an expected call of ReceivedMessages +func (mr *MockP2PNetworkMockRecorder) ReceivedMessages() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceivedMessages", reflect.TypeOf((*MockP2PNetwork)(nil).ReceivedMessages)) +} + +// Send mocks base method +func (m *MockP2PNetwork) Send(peer PeerID, message *transport.NetworkMessage) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Send", peer, message) + ret0, _ := ret[0].(error) + return ret0 +} + +// Send indicates an expected call of Send +func (mr *MockP2PNetworkMockRecorder) Send(peer, message interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockP2PNetwork)(nil).Send), peer, message) +} + +// Broadcast mocks base method +func (m *MockP2PNetwork) Broadcast(message *transport.NetworkMessage) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Broadcast", message) +} + +// Broadcast indicates an expected call of Broadcast +func (mr *MockP2PNetworkMockRecorder) Broadcast(message interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Broadcast", reflect.TypeOf((*MockP2PNetwork)(nil).Broadcast), message) +} + +// Peers mocks base method +func (m *MockP2PNetwork) Peers() []PeerID { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Peers") + ret0, _ := ret[0].([]PeerID) + return ret0 +} + +// Peers indicates an expected call of Peers +func (mr *MockP2PNetworkMockRecorder) Peers() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peers", reflect.TypeOf((*MockP2PNetwork)(nil).Peers)) +} + +// MockMessageQueue is a mock of MessageQueue interface +type MockMessageQueue struct { + ctrl *gomock.Controller + recorder *MockMessageQueueMockRecorder +} + +// MockMessageQueueMockRecorder is the mock recorder for MockMessageQueue +type MockMessageQueueMockRecorder struct { + mock *MockMessageQueue +} + +// NewMockMessageQueue creates a new mock instance +func NewMockMessageQueue(ctrl *gomock.Controller) *MockMessageQueue { + mock := &MockMessageQueue{ctrl: ctrl} + mock.recorder = &MockMessageQueueMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockMessageQueue) EXPECT() *MockMessageQueueMockRecorder { + return m.recorder +} + +// Get mocks base method +func (m *MockMessageQueue) Get() PeerMessage { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get") + ret0, _ := ret[0].(PeerMessage) + return ret0 +} + +// Get indicates an expected call of Get +func (mr *MockMessageQueueMockRecorder) Get() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMessageQueue)(nil).Get)) +} diff --git a/network/p2p/stats.go b/network/p2p/stats.go new file mode 100644 index 0000000000..52b3291619 --- /dev/null +++ b/network/p2p/stats.go @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package p2p + +import ( + "fmt" + "sort" + "strings" +) + +type NumberOfPeersStatistic struct { + NumberOfPeers int +} + +func (n NumberOfPeersStatistic) Name() string { + return "[P2P Network] Connected peers #" +} + +func (n NumberOfPeersStatistic) String() string { + return fmt.Sprintf("%d", n.NumberOfPeers) +} + +type PeersStatistic struct { + Peers []PeerID +} + +func (p PeersStatistic) Name() string { + return "[P2P Network] Connected peers" +} + +func (p PeersStatistic) String() string { + addrs := make([]string, len(p.Peers)) + for i, peer := range p.Peers { + addrs[i] = peer.String() + } + // Sort for stable order (easier for humans to understand) + sort.Slice(addrs, func(i, j int) bool { + return addrs[i] > addrs[j] + }) + return strings.Join(addrs, " ") +} diff --git a/network/proto/handlers.go b/network/proto/handlers.go new file mode 100644 index 0000000000..e7fa5162a5 --- /dev/null +++ b/network/proto/handlers.go @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package proto + +import ( + "fmt" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + log "github.com/nuts-foundation/nuts-node/network/log" + "github.com/nuts-foundation/nuts-node/network/p2p" + "github.com/nuts-foundation/nuts-node/network/transport" +) + +func (p *protocol) handleAdvertHashes(peer p2p.PeerID, advertHash *transport.AdvertHashes) { + log.Logger().Tracef("Received adverted hash from peer: %s", peer) + hashes := make([]hash.SHA256Hash, len(advertHash.Hashes)) + for i, h := range advertHash.Hashes { + hashes[i] = hash.FromSlice(h) + } + peerHash := PeerHash{ + Peer: peer, + Hashes: hashes, + } + p.newPeerHashChannel <- peerHash + + heads := p.graph.Heads() + for _, peerHash := range hashes { + found := false + for _, head := range heads { + if peerHash.Equals(head) { + found = true + break + } + } + if !found { + log.Logger().Infof("Peer has different heads than us, querying document list (peer=%s)", peer) + go p.queryDocumentList(peer) + return + } + } +} + +func (p protocol) queryDocumentList(peer p2p.PeerID) { + msg := createMessage() + msg.DocumentListQuery = &transport.DocumentListQuery{} + if err := p.p2pNetwork.Send(peer, &msg); err != nil { + log.Logger().Errorf("Unable to query peer for hash list (peer=%s): %v", peer, err) + } +} + +func (p protocol) advertHashes() { + msg := createMessage() + heads := p.graph.Heads() + slices := make([][]byte, len(heads)) + for i, hash := range heads { + slices[i] = hash.Slice() + } + log.Logger().Tracef("Broadcasting heads: %v", heads) + msg.AdvertHashes = &transport.AdvertHashes{Hashes: slices} + p.p2pNetwork.Broadcast(&msg) +} + +func (p *protocol) handleDocumentPayload(peer p2p.PeerID, contents *transport.DocumentPayload) { + payloadHash := hash.FromSlice(contents.PayloadHash) + log.Logger().Infof("Received document payload from peer (peer=%s,payloadHash=%s,len=%d)", peer, payloadHash, len(contents.Data)) + // TODO: Maybe this should be asynchronous since writing the document contents might be I/O heavy? + if document, err := p.graph.GetByPayloadHash(payloadHash); err != nil { + log.Logger().Errorf("Error while looking up document to write payload (payloadHash=%s): %v", payloadHash, err) + } else if document == nil { + log.Logger().Warnf("Received document payload for document we don't have (payloadHash=%s)", payloadHash) + } else if hasPayload, err := p.payloadStore.IsPayloadPresent(payloadHash); err != nil { + log.Logger().Errorf("Error while checking whether we already have payload (payloadHash=%s): %v", payloadHash, err) + } else if hasPayload { + log.Logger().Debugf("Received payload we already have (payloadHash=%s)", payloadHash) + } else if err := p.payloadStore.WritePayload(payloadHash, contents.Data); err != nil { + log.Logger().Errorf("Error while writing payload for document (hash=%s): %v", payloadHash, err) + } else { + // TODO: Publish change to subscribers + } +} + +func (p *protocol) handleDocumentPayloadQuery(peer p2p.PeerID, query *transport.DocumentPayloadQuery) error { + payloadHash := hash.FromSlice(query.PayloadHash) + log.Logger().Tracef("Received document payload query from peer (peer=%s, payloadHash=%s)", peer, payloadHash) + // TODO: Maybe this should be asynchronous since loading document contents might be I/O heavy? + if data, err := p.payloadStore.ReadPayload(payloadHash); err != nil { + return err + } else if data != nil { + responseMsg := createMessage() + responseMsg.DocumentPayload = &transport.DocumentPayload{ + PayloadHash: payloadHash.Slice(), + Data: data, + } + if err := p.p2pNetwork.Send(peer, &responseMsg); err != nil { + return err + } + } else { + log.Logger().Warnf("Peer queried us for document payload, but seems like we don't have it (peer=%s,payloadHash=%s)", peer, payloadHash) + } + return nil +} + +func (p *protocol) handleDocumentList(peer p2p.PeerID, documentList *transport.DocumentList) error { + log.Logger().Tracef("Received document list from peer (peer=%s)", peer) + for _, current := range documentList.Documents { + documentRef := hash.FromSlice(current.Hash) + if !documentRef.Equals(hash.SHA256Sum(current.Data)) { + log.Logger().Warn("Received document hash doesn't match document bytes, skipping.") + continue + } + if err := p.checkDocumentOnLocalNode(peer, documentRef, current.Data); err != nil { + log.Logger().Errorf("Error while checking peer document on local node (peer=%s, document=%s): %v", peer, documentRef, err) + } + } + return nil +} + +// checkDocumentOnLocalNode checks whether the given document is present on the local node, adds it if not and/or queries +// the payload if it (the payload) it not present. If we have both document and payload, nothing is done. +func (p *protocol) checkDocumentOnLocalNode(peer p2p.PeerID, documentRef hash.SHA256Hash, data []byte) error { + // TODO: Make this a bit smarter. + var document dag.Document + var err error + if document, err = dag.ParseDocument(data); err != nil { + return fmt.Errorf("received document is invalid (peer=%s,pref=%s): %w", peer, documentRef, err) + } + queryContents := false + if present, err := p.graph.IsPresent(documentRef); err != nil { + return err + } else if !present { + if err := p.graph.Add(document); err != nil { + return fmt.Errorf("unable to add received document to DAG: %w", err) + } + queryContents = true + } else if payloadPresent, err := p.payloadStore.IsPayloadPresent(document.Payload()); err != nil { + return err + } else { + queryContents = !payloadPresent + } + if queryContents { + // TODO: Currently we send the query to the peer that sent us the hash, but this peer might not have the + // document contents. We need a smarter way to get it from a peer who does. + log.Logger().Infof("Received document hash from peer that we don't have yet or we're missing its contents, will query it (peer=%s,hash=%s)", peer, documentRef) + responseMsg := createMessage() + responseMsg.DocumentPayloadQuery = &transport.DocumentPayloadQuery{PayloadHash: document.Payload().Slice()} + return p.p2pNetwork.Send(peer, &responseMsg) + } + return nil +} + +func (p *protocol) handleDocumentListQuery(peer p2p.PeerID) error { + log.Logger().Tracef("Received document list query from peer (peer=%s)", peer) + msg := createMessage() + documents, err := p.graph.All() + if err != nil { + return err + } + msg.DocumentList = &transport.DocumentList{ + Documents: make([]*transport.Document, len(documents)), + } + for i, document := range documents { + msg.DocumentList.Documents[i] = &transport.Document{ + Hash: document.Ref().Slice(), + Data: document.Data(), + } + } + if err := p.p2pNetwork.Send(peer, &msg); err != nil { + return err + } + return nil +} diff --git a/network/proto/impl.go b/network/proto/impl.go new file mode 100644 index 0000000000..ff0792360d --- /dev/null +++ b/network/proto/impl.go @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package proto + +import ( + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + log "github.com/nuts-foundation/nuts-node/network/log" + "github.com/nuts-foundation/nuts-node/network/p2p" + "github.com/nuts-foundation/nuts-node/network/transport" + "time" +) + +// protocol is thread-safe when callers use the Protocol interface +type protocol struct { + p2pNetwork p2p.P2PNetwork + graph dag.DAG + payloadStore dag.PayloadStore + // TODO: What if no-one is actually listening to this queue? Maybe we should create it when someone asks for it (lazy initialization)? + receivedPeerHashes *chanPeerHashQueue + receivedDocumentHashes *chanPeerHashQueue + peerHashes map[p2p.PeerID][]hash.SHA256Hash + + // Cache statistics to avoid having to lock precious resources + peerConsistencyHashStatistic peerConsistencyHashStatistic + newPeerHashChannel chan PeerHash + + advertHashesInterval time.Duration + // peerID contains our own peer ID which can be logged for debugging purposes + peerID p2p.PeerID +} + +func (p *protocol) Diagnostics() []core.DiagnosticResult { + return []core.DiagnosticResult{ + &p.peerConsistencyHashStatistic, + } +} + +func NewProtocol() Protocol { + p := &protocol{ + peerHashes: make(map[p2p.PeerID][]hash.SHA256Hash), + newPeerHashChannel: make(chan PeerHash, 100), + peerConsistencyHashStatistic: newPeerConsistencyHashStatistic(), + } + return p +} + +func (p *protocol) Configure(p2pNetwork p2p.P2PNetwork, graph dag.DAG, payloadStore dag.PayloadStore, advertHashesInterval time.Duration, peerID p2p.PeerID) { + p.p2pNetwork = p2pNetwork + p.graph = graph + p.payloadStore = payloadStore + p.advertHashesInterval = advertHashesInterval + p.peerID = peerID +} + +func (p *protocol) Start() { + go p.consumeMessages(p.p2pNetwork.ReceivedMessages()) + go p.updateDiagnostics() + go p.startAdvertingHashes() +} + +func (p protocol) Stop() { + +} + +func (p protocol) startAdvertingHashes() { + ticker := time.NewTicker(p.advertHashesInterval) + for { + select { + case <-ticker.C: + p.advertHashes() + } + } +} + +func (p *protocol) updateDiagnostics() { + // TODO: When to exit the loop? + ticker := time.NewTicker(2 * time.Second) + for { + select { + case <-ticker.C: + // TODO: Make this garbage collection less dumb. Maybe we should be notified of disconnects rather than looping each time + connectedPeers := p.p2pNetwork.Peers() + var changed = false + for peerId := range p.peerHashes { + var present = false + for _, connectedPeer := range connectedPeers { + if peerId == connectedPeer { + present = true + } + } + if !present { + delete(p.peerHashes, peerId) + changed = true + } + } + if changed { + p.peerConsistencyHashStatistic.copyFrom(p.peerHashes) + } + case peerHash := <-p.newPeerHashChannel: + p.peerHashes[peerHash.Peer] = peerHash.Hashes + p.peerConsistencyHashStatistic.copyFrom(p.peerHashes) + } + } +} + +func (p protocol) consumeMessages(queue p2p.MessageQueue) { + for { + peerMsg := queue.Get() + msg := peerMsg.Message + var err error + if msg.Header == nil { + err = ErrMissingProtocolVersion + } else if msg.Header.Version != Version { + err = ErrUnsupportedProtocolVersion + } else { + err = p.handleMessage(peerMsg) + } + if err != nil { + log.Logger().Errorf("Error handling message (peer=%s): %v", peerMsg.Peer, err) + } + } +} + +func (p *protocol) handleMessage(peerMsg p2p.PeerMessage) error { + peer := peerMsg.Peer + msg := peerMsg.Message + if msg.AdvertHashes != nil { + p.handleAdvertHashes(peer, msg.AdvertHashes) + } + if msg.DocumentListQuery != nil { + if err := p.handleDocumentListQuery(peer); err != nil { + return err + } + } + if msg.DocumentList != nil { + if err := p.handleDocumentList(peer, msg.DocumentList); err != nil { + return err + } + } + if msg.DocumentPayloadQuery != nil && msg.DocumentPayloadQuery.PayloadHash != nil { + if err := p.handleDocumentPayloadQuery(peer, msg.DocumentPayloadQuery); err != nil { + return err + } + } + if msg.DocumentPayload != nil && msg.DocumentPayload.PayloadHash != nil && msg.DocumentPayload.Data != nil { + p.handleDocumentPayload(peer, msg.DocumentPayload) + } + return nil +} + +func createMessage() transport.NetworkMessage { + return transport.NetworkMessage{ + Header: &transport.Header{ + Version: Version, + }, + } +} diff --git a/network/proto/interface.go b/network/proto/interface.go new file mode 100644 index 0000000000..e070789a19 --- /dev/null +++ b/network/proto/interface.go @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package proto + +import ( + "errors" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/dag" + "github.com/nuts-foundation/nuts-node/network/p2p" + "time" +) + +// Version holds the number of the version of this protocol implementation. +const Version = 1 + +// ErrMissingProtocolVersion is used when a message is received without protocol version. +var ErrMissingProtocolVersion = errors.New("missing protocol version") + +// ErrUnsupportedProtocolVersion is used when a message is received with an unsupported protocol version. +var ErrUnsupportedProtocolVersion = errors.New("unsupported protocol version") + +// Protocol defines the API for the protocol layer, which is a high-level interface to interact with the network. It responds +// from (peer) messages received through the P2P layer. +type Protocol interface { + core.Diagnosable + // Configure configures the Protocol. Must be called before Start(). + Configure(p2pNetwork p2p.P2PNetwork, graph dag.DAG, payloadStore dag.PayloadStore, advertHashesInterval time.Duration, peerID p2p.PeerID) + // Starts the Protocol (sending and receiving of messages). + Start() + // Stops the Protocol. + Stop() +} + +// PeerHashQueue is a queue which contains the hashes adverted by our peers. It's a FILO queue, since +// the hashes represent append-only data structures which means the last one is most recent. +type PeerHashQueue interface { + // Get blocks until there's an PeerHash available and returns it. + Get() *PeerHash +} + +// PeerHash describes a hash we received from a peer. +type PeerHash struct { + // Peer holds the ID of the peer we got the hash from. + Peer p2p.PeerID + // Hashes holds the hashes we received. + Hashes []hash.SHA256Hash +} + +type chanPeerHashQueue struct { + c chan *PeerHash +} + +func (q chanPeerHashQueue) Get() *PeerHash { + return <-q.c +} diff --git a/network/proto/mock.go b/network/proto/mock.go new file mode 100644 index 0000000000..b153f0a6ad --- /dev/null +++ b/network/proto/mock.go @@ -0,0 +1,124 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: network/proto/interface.go + +// Package proto is a generated GoMock package. +package proto + +import ( + gomock "github.com/golang/mock/gomock" + core "github.com/nuts-foundation/nuts-node/core" + dag "github.com/nuts-foundation/nuts-node/network/dag" + p2p "github.com/nuts-foundation/nuts-node/network/p2p" + reflect "reflect" + time "time" +) + +// MockProtocol is a mock of Protocol interface +type MockProtocol struct { + ctrl *gomock.Controller + recorder *MockProtocolMockRecorder +} + +// MockProtocolMockRecorder is the mock recorder for MockProtocol +type MockProtocolMockRecorder struct { + mock *MockProtocol +} + +// NewMockProtocol creates a new mock instance +func NewMockProtocol(ctrl *gomock.Controller) *MockProtocol { + mock := &MockProtocol{ctrl: ctrl} + mock.recorder = &MockProtocolMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockProtocol) EXPECT() *MockProtocolMockRecorder { + return m.recorder +} + +// Diagnostics mocks base method +func (m *MockProtocol) Diagnostics() []core.DiagnosticResult { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Diagnostics") + ret0, _ := ret[0].([]core.DiagnosticResult) + return ret0 +} + +// Diagnostics indicates an expected call of Diagnostics +func (mr *MockProtocolMockRecorder) Diagnostics() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Diagnostics", reflect.TypeOf((*MockProtocol)(nil).Diagnostics)) +} + +// Configure mocks base method +func (m *MockProtocol) Configure(p2pNetwork p2p.P2PNetwork, graph dag.DAG, payloadStore dag.PayloadStore, advertHashesInterval time.Duration, peerID p2p.PeerID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Configure", p2pNetwork, graph, payloadStore, advertHashesInterval, peerID) +} + +// Configure indicates an expected call of Configure +func (mr *MockProtocolMockRecorder) Configure(p2pNetwork, graph, payloadStore, advertHashesInterval, peerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configure", reflect.TypeOf((*MockProtocol)(nil).Configure), p2pNetwork, graph, payloadStore, advertHashesInterval, peerID) +} + +// Start mocks base method +func (m *MockProtocol) Start() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Start") +} + +// Start indicates an expected call of Start +func (mr *MockProtocolMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockProtocol)(nil).Start)) +} + +// Stop mocks base method +func (m *MockProtocol) Stop() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Stop") +} + +// Stop indicates an expected call of Stop +func (mr *MockProtocolMockRecorder) Stop() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockProtocol)(nil).Stop)) +} + +// MockPeerHashQueue is a mock of PeerHashQueue interface +type MockPeerHashQueue struct { + ctrl *gomock.Controller + recorder *MockPeerHashQueueMockRecorder +} + +// MockPeerHashQueueMockRecorder is the mock recorder for MockPeerHashQueue +type MockPeerHashQueueMockRecorder struct { + mock *MockPeerHashQueue +} + +// NewMockPeerHashQueue creates a new mock instance +func NewMockPeerHashQueue(ctrl *gomock.Controller) *MockPeerHashQueue { + mock := &MockPeerHashQueue{ctrl: ctrl} + mock.recorder = &MockPeerHashQueueMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockPeerHashQueue) EXPECT() *MockPeerHashQueueMockRecorder { + return m.recorder +} + +// Get mocks base method +func (m *MockPeerHashQueue) Get() *PeerHash { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get") + ret0, _ := ret[0].(*PeerHash) + return ret0 +} + +// Get indicates an expected call of Get +func (mr *MockPeerHashQueueMockRecorder) Get() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPeerHashQueue)(nil).Get)) +} diff --git a/network/proto/stats.go b/network/proto/stats.go new file mode 100644 index 0000000000..d03a6406af --- /dev/null +++ b/network/proto/stats.go @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package proto + +import ( + "fmt" + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/p2p" + "sort" + "strings" + "sync" +) + +func newPeerConsistencyHashStatistic() peerConsistencyHashStatistic { + return peerConsistencyHashStatistic{ + peerHashes: new(map[p2p.PeerID][]hash.SHA256Hash), + mux: &sync.Mutex{}, + } +} + +type peerConsistencyHashStatistic struct { + peerHashes *map[p2p.PeerID][]hash.SHA256Hash + mux *sync.Mutex +} + +func (d peerConsistencyHashStatistic) Name() string { + return "[Protocol] Peer hashes" +} + +func (d peerConsistencyHashStatistic) String() string { + d.mux.Lock() + defer d.mux.Unlock() + var groupedByHash = make(map[string][]string) + for peer, hashes := range *d.peerHashes { + for _, hash := range hashes { + groupedByHash[hash.String()] = append(groupedByHash[hash.String()], string(peer)) + } + } + var items []string + for hash, peers := range groupedByHash { + // Sort for stable order (easier for humans to understand) + sort.Slice(peers, func(i, j int) bool { + return peers[i] > peers[j] + }) + items = append(items, fmt.Sprintf("%s={%s}", hash, strings.Join(peers, ", "))) + } + // Sort for stable order (easier for humans to understand) + sort.Slice(items, func(i, j int) bool { + return items[i] > items[j] + }) + return strings.Join(items, ", ") +} + +func (d peerConsistencyHashStatistic) copyFrom(input map[p2p.PeerID][]hash.SHA256Hash) { + d.mux.Lock() + defer d.mux.Unlock() + var newMap = make(map[p2p.PeerID][]hash.SHA256Hash, len(input)) + for k, v := range input { + cp := make([]hash.SHA256Hash, len(v)) + copy(cp, v) + newMap[k] = cp + } + *d.peerHashes = newMap +} diff --git a/network/proto/stats_test.go b/network/proto/stats_test.go new file mode 100644 index 0000000000..bef63e4bd3 --- /dev/null +++ b/network/proto/stats_test.go @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package proto + +import ( + "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/network/p2p" + "github.com/stretchr/testify/assert" + "sync" + "testing" +) + +func TestPeerConsistencyHashStatistic(t *testing.T) { + diagnostic := peerConsistencyHashStatistic{peerHashes: new(map[p2p.PeerID][]hash.SHA256Hash), mux: &sync.Mutex{}} + diagnostic.copyFrom(map[p2p.PeerID][]hash.SHA256Hash{"abc": {hash.FromSlice([]byte{1, 2, 3})}}) + assert.Equal(t, diagnostic.String(), "0102030000000000000000000000000000000000000000000000000000000000={abc}") +} diff --git a/network/test.go b/network/test.go new file mode 100644 index 0000000000..c166b5ef4a --- /dev/null +++ b/network/test.go @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package network + +import ( + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/sirupsen/logrus" + "path" +) + +// NewTestNetworkInstance creates a new Network instance that writes it data to a test directory. +func NewTestNetworkInstance(testDirectory string) *NetworkEngine { + config := TestNetworkConfig(testDirectory) + newInstance := NewNetworkInstance(config, crypto.NewTestCryptoInstance(testDirectory)) + if err := newInstance.Configure(); err != nil { + logrus.Fatal(err) + } + return newInstance +} + +// NewTestNetworkInstance creates new network config with a test directory as data path. +func TestNetworkConfig(testDirectory string) Config { + config := DefaultConfig() + config.DatabaseFile = path.Join(testDirectory, "network.db") + config.GrpcAddr = ":5555" + config.EnableTLS = false + config.PublicAddr = "test:5555" + return config +} diff --git a/network/test/certificate-and-key.pem b/network/test/certificate-and-key.pem new file mode 100644 index 0000000000..3da1e802b4 --- /dev/null +++ b/network/test/certificate-and-key.pem @@ -0,0 +1,46 @@ +-----BEGIN CERTIFICATE----- +MIIDJjCCAg6gAwIBAgIJAJES+D3F7kfeMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV +BAMMB1Jvb3QgQ0EwHhcNMjEwMTI2MTE1MzUwWhcNMjMwNTAxMTE1MzUwWjAUMRIw +EAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDVd35cCx0ald4nLbBKRli8Hfsl0uuEzkfGRtZKKW1pYJ4f/OpHwdW7Kvu17pqo +t2VMH+VCcZNfqh4MyDeuGocLkqm8Rj13jUZrJbvbTqzzj3685BWFsNg/TUAmEUBi +9wjKbaJgMcR0TTd4Ab9ux6qgIltR0kGM8v8I3kuEskToSkUJeCBxravvRUvmpe/F +5V1XdYvh+ckX8i8PujmzwWVezV5vT1wXKTVgSfhE+U/C1iNlUS58rbxQ76X40Y6g +4SMqdtyhsN1L4vYYrmeFiXqGCN2kwoXdIKLec7btdi3A/JgBypigwfR4pOqaBLsr +t1Vh+80QRqU39Jlq+HHT2bWnAgMBAAGjfTB7MCwGA1UdIwQlMCOhFqQUMBIxEDAO +BgNVBAMMB1Jvb3QgQ0GCCQCcbryLrV3pnTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE +8DAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwFAYDVR0RBA0wC4IJbG9j +YWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCgUqk+5JyMnc9u8YVex3byVwoqBzsb +6Ni/TjDRsuFNIdJk4DPogF6Uzfc7tMr/nmFtiWNwrkOwjvC2StXUeCwVt6Sj51oj +qMLwpds9lcJZelsO/rar0mIiuradUUrISz9DTBqC/aE2hsRw0i4m/wF+slVQY7Aa +ZnECVkHdrKGz6OMFF8uU9t7N+xbzx5nFswEbJXw4AjTklXlyHeyuC0y09ZmWcUDs +16Gop6VMff6NkShfyUP3EPtvR4Mr33BDAXl8ePp6BFQFd1+IzBY//gfnNBObOqlA +zG0zvbFZM8oAu/AWf85MH4Ex06cbsimNUsJqu/cx4rDzqNF5iC2uKfKJ +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA1Xd+XAsdGpXeJy2wSkZYvB37JdLrhM5HxkbWSiltaWCeH/zq +R8HVuyr7te6aqLdlTB/lQnGTX6oeDMg3rhqHC5KpvEY9d41GayW7206s849+vOQV +hbDYP01AJhFAYvcIym2iYDHEdE03eAG/bseqoCJbUdJBjPL/CN5LhLJE6EpFCXgg +ca2r70VL5qXvxeVdV3WL4fnJF/IvD7o5s8FlXs1eb09cFyk1YEn4RPlPwtYjZVEu +fK28UO+l+NGOoOEjKnbcobDdS+L2GK5nhYl6hgjdpMKF3SCi3nO27XYtwPyYAcqY +oMH0eKTqmgS7K7dVYfvNEEalN/SZavhx09m1pwIDAQABAoIBAE86XofDCDZJ0X4x +EvE+UsjfR8uuEQPlm/YXbIlx/s3Rsl6k/D9Nzgn91hacTIK6LrqmS+zxQoWLGkQc +O64+4kiFUsIyP7YTlUNHagXnmYF8jMmGzgYksN4tydVXKdKRZY3cSxvw3L1du2yr +GWYAbW+p0ML5iqLJvaaI+vZxQ+oCmTm2pBwDBcZwWm+if9rtjGyGC/QbDnTDRRQL +jBVRK4fvvOB/TzpcKWsea5gflqBnEDZ85V2vAy/evx13ZgLl0+NjGSEKPjldgZ09 +oZAEU/rFF3dQExHg6DGsFuQe0BmZb87/fxJI3oVfn/4f9BZ/ETm8EOiEGU2zLGMU +aGfC++ECgYEA7PIW5Rhjvfn1buQZ8m+3LIMxkVoCaeMjLyafimPTz9OklO0xT02F +BL9cWiZikjCDix/H2IjJ2WXzAt9NefWl2zBM4akhwegboyGtncLywK5k8+y/yfzy +GXjbyprji9oR4F328OIkkiupaxuXZKfB8fEaLZpKzlxtdLKJE6UFq50CgYEA5qIN +mvfh4r6lWxg3ts6RIO+wCkDUnSKFDjswfYr6lWfYtSFU0qHdZ2f4iOAKD58PhpLx +kqGgRG3vN+pBUXoP+YkDqlnloHd7xK+HLbnwT64JLZO6DA1wD9+3vllVKqIR5WVx +wTzIvJ7BxE5kugKZaBA1v+meBmjjnIz7LpFjDRMCgYAmkOTPFyAs7MFY8kGS50IO +ObBYsyjPaUvxwbyX/tWb7xvLraun97sd4bO7bKIAn2rZuyuBAAqymthp8EShBC2h +toPc+vVmpUvSSooCspdmazw9Q5yX2Nbi9Hv5xyogOjdMqJ6n6HcBX5/ssgn7NR7p +LVAQehuQ6RRbuS98hhCdNQKBgFKr3DykhnAE7rkMoUwCF7u6r1u9iXkaGp/TT7pw +yworQ18KJ6GpK/gZKNnHlVOsLKCMo9Nv5EcjMRDWA7v1CSzllE7IEqvGqLMESGx3 +rlChjeCsh5AycOz/wJmW5BR4K/oStwgRhdM3BTYc87ZJoDvRM7MrRt39UzmWv6Md +smfJAoGABxNk3twM/uOQBB14Fv5vkERYhu+pyqzpTKksHTArl6BVbw2o3L7jNk3C +lzOgZjM75pvZuQZLMdro/tMx49BY0M8UpW7qL3IhdGJdcwAEzl1v8WB9P5pp5Rhu +J4sCzZBDZ7nxRCVMJUT/ytEYVS3/b/Yxaq0SIkGs07roEcwcQso= +-----END RSA PRIVATE KEY----- diff --git a/network/test/generate.sh b/network/test/generate.sh new file mode 100755 index 0000000000..e5ad1e7876 --- /dev/null +++ b/network/test/generate.sh @@ -0,0 +1,21 @@ +# Generate CA +openssl genrsa -out ca.key 2048 +openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 -out ca.pem -subj "/CN=Root CA" + +# Generate node +openssl genrsa -out localhost.key 2048 +openssl req -new -key localhost.key -out localhost.csr -subj "/CN=localhost" +openssl x509 -req -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial \ +-out localhost.pem -days 825 -sha256 -extfile localhost.ext + +# Copy and clean up +cat localhost.pem > certificate-and-key.pem +cat localhost.key >> certificate-and-key.pem +cat ca.pem > truststore.pem + +rm localhost.csr +rm localhost.pem +rm localhost.key +rm ca.pem +rm ca.key +rm ca.srl \ No newline at end of file diff --git a/network/test/localhost.ext b/network/test/localhost.ext new file mode 100644 index 0000000000..b7ca9cc0b1 --- /dev/null +++ b/network/test/localhost.ext @@ -0,0 +1,8 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +extendedKeyUsage = serverAuth, clientAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost \ No newline at end of file diff --git a/network/test/truststore.pem b/network/test/truststore.pem new file mode 100644 index 0000000000..bff6dee4b2 --- /dev/null +++ b/network/test/truststore.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICoDCCAYgCCQCcbryLrV3pnTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdS +b290IENBMB4XDTIxMDEyNjExNTM1MFoXDTI2MDEyNTExNTM1MFowEjEQMA4GA1UE +AwwHUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL/xOZ0K +dmq8ERmEJQyD15nJQrBvCG8PN5fRWQVun8dMwB40qmGJp1BJkdq7W7omr9qkxQv5 +tQYjJr3/qNrcSvQpDeXr09E+dPhQokrIlPb7WV/5LIjF7PE3psFWCX4Q/FI8TUwH +iSUvg0siwFwNRbu3MlnCCPcEuOYa5uq9ybQ+wldsSHAvfmQ79xGOrGYSDxSjdAzu +fm7tuYBfFeRPk1Uo31yI3tzZI9dcY3cGXDA1PV0rL0+/njJ6bnBeEydN1PdmYqFw +6EPkm9nTnpjBBfnzh9a2MYYzp1KgY2CLqddme3tBr67stAiEmDKgnMGz/IVwqyPO +djj8VvCf7hMMsU8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAvQ+M+xTptWPEY0ys +kh3RDONcF4tftUBSIl66SblzP5qxJhhAJL6T7FfTKokssZGJOjHq+774Ojeh5n+l +mOHIM9f9dXup0S/iHuyVpnJ3OKN2u6kL4NmUp4eRuTIyCT+PuEhRsZr8Tt7BfuuF +m3G9X3RfU6nTFN5dnckBFxS3biwpoFS21X+FKsujTw5SzABktFhkcw6Ibk8V4MTG +n/eVzaemzMTmPGCoku9ch6NrG1638yQPPda/E18rFHUqJjgJvz5GR6eDyT2D5wzJ +jtdbRMm8G2pUcKBuUL828xXeddpRqmnRsI1F0Tj9OReRJJxoMvxWpBGBqZRVwvGh +GKheyQ== +-----END CERTIFICATE----- diff --git a/network/transport/network.pb.go b/network/transport/network.pb.go new file mode 100644 index 0000000000..fa95fc5e68 --- /dev/null +++ b/network/transport/network.pb.go @@ -0,0 +1,818 @@ +// +// Copyright (C) 2021. Nuts community +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/network.proto + +package transport + +import ( + context "context" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +// We multiplex all of our messages over a single super-message type, because we're using streams. If we did not do that, +// we'd need to open a stream per operation, which would make the number of required streams explode (since we're +// building a full mesh network). +type NetworkMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Header *Header `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` + AdvertHashes *AdvertHashes `protobuf:"bytes,100,opt,name=advertHashes,proto3" json:"advertHashes,omitempty"` + DocumentListQuery *DocumentListQuery `protobuf:"bytes,101,opt,name=DocumentListQuery,proto3" json:"DocumentListQuery,omitempty"` + DocumentList *DocumentList `protobuf:"bytes,102,opt,name=DocumentList,proto3" json:"DocumentList,omitempty"` + DocumentPayloadQuery *DocumentPayloadQuery `protobuf:"bytes,103,opt,name=documentPayloadQuery,proto3" json:"documentPayloadQuery,omitempty"` + DocumentPayload *DocumentPayload `protobuf:"bytes,104,opt,name=documentPayload,proto3" json:"documentPayload,omitempty"` +} + +func (x *NetworkMessage) Reset() { + *x = NetworkMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_network_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NetworkMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkMessage) ProtoMessage() {} + +func (x *NetworkMessage) ProtoReflect() protoreflect.Message { + mi := &file_transport_network_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkMessage.ProtoReflect.Descriptor instead. +func (*NetworkMessage) Descriptor() ([]byte, []int) { + return file_transport_network_proto_rawDescGZIP(), []int{0} +} + +func (x *NetworkMessage) GetHeader() *Header { + if x != nil { + return x.Header + } + return nil +} + +func (x *NetworkMessage) GetAdvertHashes() *AdvertHashes { + if x != nil { + return x.AdvertHashes + } + return nil +} + +func (x *NetworkMessage) GetDocumentListQuery() *DocumentListQuery { + if x != nil { + return x.DocumentListQuery + } + return nil +} + +func (x *NetworkMessage) GetDocumentList() *DocumentList { + if x != nil { + return x.DocumentList + } + return nil +} + +func (x *NetworkMessage) GetDocumentPayloadQuery() *DocumentPayloadQuery { + if x != nil { + return x.DocumentPayloadQuery + } + return nil +} + +func (x *NetworkMessage) GetDocumentPayload() *DocumentPayload { + if x != nil { + return x.DocumentPayload + } + return nil +} + +// Metadata types +type Header struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *Header) Reset() { + *x = Header{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_network_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_transport_network_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_transport_network_proto_rawDescGZIP(), []int{1} +} + +func (x *Header) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +// Actual messages go here +type AdvertHashes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hashes [][]byte `protobuf:"bytes,1,rep,name=hashes,proto3" json:"hashes,omitempty"` +} + +func (x *AdvertHashes) Reset() { + *x = AdvertHashes{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_network_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AdvertHashes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdvertHashes) ProtoMessage() {} + +func (x *AdvertHashes) ProtoReflect() protoreflect.Message { + mi := &file_transport_network_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdvertHashes.ProtoReflect.Descriptor instead. +func (*AdvertHashes) Descriptor() ([]byte, []int) { + return file_transport_network_proto_rawDescGZIP(), []int{2} +} + +func (x *AdvertHashes) GetHashes() [][]byte { + if x != nil { + return x.Hashes + } + return nil +} + +// Message to ask for a peer's hash list +type DocumentListQuery struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *DocumentListQuery) Reset() { + *x = DocumentListQuery{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_network_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DocumentListQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocumentListQuery) ProtoMessage() {} + +func (x *DocumentListQuery) ProtoReflect() protoreflect.Message { + mi := &file_transport_network_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocumentListQuery.ProtoReflect.Descriptor instead. +func (*DocumentListQuery) Descriptor() ([]byte, []int) { + return file_transport_network_proto_rawDescGZIP(), []int{3} +} + +// Message to inform a peer of our hash list +type DocumentList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Documents []*Document `protobuf:"bytes,1,rep,name=documents,proto3" json:"documents,omitempty"` +} + +func (x *DocumentList) Reset() { + *x = DocumentList{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_network_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DocumentList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocumentList) ProtoMessage() {} + +func (x *DocumentList) ProtoReflect() protoreflect.Message { + mi := &file_transport_network_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocumentList.ProtoReflect.Descriptor instead. +func (*DocumentList) Descriptor() ([]byte, []int) { + return file_transport_network_proto_rawDescGZIP(), []int{4} +} + +func (x *DocumentList) GetDocuments() []*Document { + if x != nil { + return x.Documents + } + return nil +} + +type Document struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *Document) Reset() { + *x = Document{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_network_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Document) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Document) ProtoMessage() {} + +func (x *Document) ProtoReflect() protoreflect.Message { + mi := &file_transport_network_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Document.ProtoReflect.Descriptor instead. +func (*Document) Descriptor() ([]byte, []int) { + return file_transport_network_proto_rawDescGZIP(), []int{5} +} + +func (x *Document) GetHash() []byte { + if x != nil { + return x.Hash + } + return nil +} + +func (x *Document) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +// Message to ask for a peer for a specific document +type DocumentPayloadQuery struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PayloadHash []byte `protobuf:"bytes,1,opt,name=payloadHash,proto3" json:"payloadHash,omitempty"` +} + +func (x *DocumentPayloadQuery) Reset() { + *x = DocumentPayloadQuery{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_network_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DocumentPayloadQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocumentPayloadQuery) ProtoMessage() {} + +func (x *DocumentPayloadQuery) ProtoReflect() protoreflect.Message { + mi := &file_transport_network_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocumentPayloadQuery.ProtoReflect.Descriptor instead. +func (*DocumentPayloadQuery) Descriptor() ([]byte, []int) { + return file_transport_network_proto_rawDescGZIP(), []int{6} +} + +func (x *DocumentPayloadQuery) GetPayloadHash() []byte { + if x != nil { + return x.PayloadHash + } + return nil +} + +type DocumentPayload struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PayloadHash []byte `protobuf:"bytes,1,opt,name=payloadHash,proto3" json:"payloadHash,omitempty"` + Data []byte `protobuf:"bytes,10,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *DocumentPayload) Reset() { + *x = DocumentPayload{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_network_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DocumentPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocumentPayload) ProtoMessage() {} + +func (x *DocumentPayload) ProtoReflect() protoreflect.Message { + mi := &file_transport_network_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocumentPayload.ProtoReflect.Descriptor instead. +func (*DocumentPayload) Descriptor() ([]byte, []int) { + return file_transport_network_proto_rawDescGZIP(), []int{7} +} + +func (x *DocumentPayload) GetPayloadHash() []byte { + if x != nil { + return x.PayloadHash + } + return nil +} + +func (x *DocumentPayload) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_transport_network_proto protoreflect.FileDescriptor + +var file_transport_network_proto_rawDesc = []byte{ + 0x0a, 0x17, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x22, 0x9c, 0x03, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x0c, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x48, 0x61, 0x73, 0x68, + 0x65, 0x73, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x41, 0x64, 0x76, 0x65, 0x72, 0x74, 0x48, 0x61, 0x73, 0x68, 0x65, + 0x73, 0x52, 0x0c, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x12, + 0x4a, 0x0a, 0x11, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x4c, + 0x69, 0x73, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x11, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, + 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x3b, 0x0a, 0x0c, 0x44, + 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x18, 0x66, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x44, 0x6f, + 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x0c, 0x44, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x53, 0x0a, 0x14, 0x64, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x14, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x44, 0x0a, + 0x0f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x52, 0x0f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x22, 0x22, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x26, 0x0a, 0x0c, 0x41, 0x64, 0x76, 0x65, 0x72, + 0x74, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x61, 0x73, 0x68, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x06, 0x68, 0x61, 0x73, 0x68, 0x65, 0x73, 0x22, + 0x13, 0x0a, 0x11, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x22, 0x41, 0x0a, 0x0c, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x4c, 0x69, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x09, 0x64, 0x6f, + 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x32, 0x0a, 0x08, 0x44, 0x6f, 0x63, 0x75, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x38, 0x0a, 0x14, 0x44, + 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x51, 0x75, + 0x65, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x61, + 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x48, 0x61, 0x73, 0x68, 0x22, 0x47, 0x0a, 0x0f, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0x50, + 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x45, 0x0a, 0x07, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x12, 0x19, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x19, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, + 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6e, + 0x75, 0x74, 0x73, 0x2d, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6e, + 0x75, 0x74, 0x73, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_transport_network_proto_rawDescOnce sync.Once + file_transport_network_proto_rawDescData = file_transport_network_proto_rawDesc +) + +func file_transport_network_proto_rawDescGZIP() []byte { + file_transport_network_proto_rawDescOnce.Do(func() { + file_transport_network_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_network_proto_rawDescData) + }) + return file_transport_network_proto_rawDescData +} + +var file_transport_network_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_transport_network_proto_goTypes = []interface{}{ + (*NetworkMessage)(nil), // 0: transport.NetworkMessage + (*Header)(nil), // 1: transport.Header + (*AdvertHashes)(nil), // 2: transport.AdvertHashes + (*DocumentListQuery)(nil), // 3: transport.DocumentListQuery + (*DocumentList)(nil), // 4: transport.DocumentList + (*Document)(nil), // 5: transport.Document + (*DocumentPayloadQuery)(nil), // 6: transport.DocumentPayloadQuery + (*DocumentPayload)(nil), // 7: transport.DocumentPayload +} +var file_transport_network_proto_depIdxs = []int32{ + 1, // 0: transport.NetworkMessage.header:type_name -> transport.Header + 2, // 1: transport.NetworkMessage.advertHashes:type_name -> transport.AdvertHashes + 3, // 2: transport.NetworkMessage.DocumentListQuery:type_name -> transport.DocumentListQuery + 4, // 3: transport.NetworkMessage.DocumentList:type_name -> transport.DocumentList + 6, // 4: transport.NetworkMessage.documentPayloadQuery:type_name -> transport.DocumentPayloadQuery + 7, // 5: transport.NetworkMessage.documentPayload:type_name -> transport.DocumentPayload + 5, // 6: transport.DocumentList.documents:type_name -> transport.Document + 0, // 7: transport.Network.Connect:input_type -> transport.NetworkMessage + 0, // 8: transport.Network.Connect:output_type -> transport.NetworkMessage + 8, // [8:9] is the sub-list for method output_type + 7, // [7:8] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_transport_network_proto_init() } +func file_transport_network_proto_init() { + if File_transport_network_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_network_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NetworkMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_network_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Header); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_network_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AdvertHashes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_network_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DocumentListQuery); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_network_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DocumentList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_network_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Document); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_network_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DocumentPayloadQuery); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_network_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DocumentPayload); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_transport_network_proto_rawDesc, + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_transport_network_proto_goTypes, + DependencyIndexes: file_transport_network_proto_depIdxs, + MessageInfos: file_transport_network_proto_msgTypes, + }.Build() + File_transport_network_proto = out.File + file_transport_network_proto_rawDesc = nil + file_transport_network_proto_goTypes = nil + file_transport_network_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// NetworkClient is the client API for Network service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type NetworkClient interface { + Connect(ctx context.Context, opts ...grpc.CallOption) (Network_ConnectClient, error) +} + +type networkClient struct { + cc grpc.ClientConnInterface +} + +func NewNetworkClient(cc grpc.ClientConnInterface) NetworkClient { + return &networkClient{cc} +} + +func (c *networkClient) Connect(ctx context.Context, opts ...grpc.CallOption) (Network_ConnectClient, error) { + stream, err := c.cc.NewStream(ctx, &_Network_serviceDesc.Streams[0], "/transport.Network/Connect", opts...) + if err != nil { + return nil, err + } + x := &networkConnectClient{stream} + return x, nil +} + +type Network_ConnectClient interface { + Send(*NetworkMessage) error + Recv() (*NetworkMessage, error) + grpc.ClientStream +} + +type networkConnectClient struct { + grpc.ClientStream +} + +func (x *networkConnectClient) Send(m *NetworkMessage) error { + return x.ClientStream.SendMsg(m) +} + +func (x *networkConnectClient) Recv() (*NetworkMessage, error) { + m := new(NetworkMessage) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// NetworkServer is the server API for Network service. +type NetworkServer interface { + Connect(Network_ConnectServer) error +} + +// UnimplementedNetworkServer can be embedded to have forward compatible implementations. +type UnimplementedNetworkServer struct { +} + +func (*UnimplementedNetworkServer) Connect(Network_ConnectServer) error { + return status.Errorf(codes.Unimplemented, "method Connect not implemented") +} + +func RegisterNetworkServer(s *grpc.Server, srv NetworkServer) { + s.RegisterService(&_Network_serviceDesc, srv) +} + +func _Network_Connect_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(NetworkServer).Connect(&networkConnectServer{stream}) +} + +type Network_ConnectServer interface { + Send(*NetworkMessage) error + Recv() (*NetworkMessage, error) + grpc.ServerStream +} + +type networkConnectServer struct { + grpc.ServerStream +} + +func (x *networkConnectServer) Send(m *NetworkMessage) error { + return x.ServerStream.SendMsg(m) +} + +func (x *networkConnectServer) Recv() (*NetworkMessage, error) { + m := new(NetworkMessage) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +var _Network_serviceDesc = grpc.ServiceDesc{ + ServiceName: "transport.Network", + HandlerType: (*NetworkServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Connect", + Handler: _Network_Connect_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "transport/network.proto", +} diff --git a/network/transport/network.proto b/network/transport/network.proto new file mode 100644 index 0000000000..0e080724bf --- /dev/null +++ b/network/transport/network.proto @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +syntax = "proto3"; + +option go_package = "github.com/nuts-foundation/nuts-node/network/transport"; + +package transport; + +service Network { + rpc Connect (stream NetworkMessage) returns (stream NetworkMessage) { + } +} + +// We multiplex all of our messages over a single super-message type, because we're using streams. If we did not do that, +// we'd need to open a stream per operation, which would make the number of required streams explode (since we're +// building a full mesh network). +message NetworkMessage { + Header header = 1; + AdvertHashes advertHashes = 100; + DocumentListQuery DocumentListQuery = 101; + DocumentList DocumentList = 102; + DocumentPayloadQuery documentPayloadQuery = 103; + DocumentPayload documentPayload = 104; +} + +// Metadata types +message Header { + uint32 version = 1; +} + +// Actual messages go here +message AdvertHashes { + repeated bytes hashes = 1; +} + +// Message to ask for a peer's hash list +message DocumentListQuery { +} + +// Message to inform a peer of our hash list +message DocumentList { + repeated Document documents = 1; +} + +message Document { + bytes hash = 1; + bytes data = 2; +} + +// Message to ask for a peer for a specific document +message DocumentPayloadQuery { + bytes payloadHash = 1; +} + +message DocumentPayload { + bytes payloadHash = 1; + bytes data = 10; +} \ No newline at end of file diff --git a/test/io/io.go b/test/io/io.go index 593acdfb84..c341c7036d 100644 --- a/test/io/io.go +++ b/test/io/io.go @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package io import ( diff --git a/test/io/io_test.go b/test/io/io_test.go index 8e4ee7ac44..86a701b2eb 100644 --- a/test/io/io_test.go +++ b/test/io/io_test.go @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package io import ( diff --git a/vdr/ambassador.go b/vdr/ambassador.go new file mode 100644 index 0000000000..a282b4fb68 --- /dev/null +++ b/vdr/ambassador.go @@ -0,0 +1,56 @@ +/* + * Nuts node + * Copyright (C) 2021 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package vdr + +import ( + "github.com/nuts-foundation/nuts-node/network" + "github.com/nuts-foundation/nuts-node/network/dag" + + "github.com/nuts-foundation/nuts-node/vdr/logging" +) + +const documentType = "nuts.registry-event" + +// Ambassador acts as integration point between VDR and network by sending DID Documents network and process +// DID Documents received through the network. +type Ambassador interface { + // Start instructs the ambassador to start receiving DID Documents from the network. + Start() +} + +type ambassador struct { + networkClient network.Network +} + +// NewAmbassador creates a new Ambassador, +func NewAmbassador(networkClient network.Network) Ambassador { + instance := &ambassador{ + networkClient: networkClient, + } + return instance +} + +// Start instructs the ambassador to start receiving DID Documents from the network. +func (n *ambassador) Start() { + n.networkClient.Subscribe(documentType, func(document dag.Document, payload []byte) error { + logging.Log().Warn("Not implemented: processing DID documents received from Nuts Network.") + return nil + }) +} diff --git a/vdr/doc-creator_test.go b/vdr/doc-creator_test.go index 0a609af46d..269f1cf344 100644 --- a/vdr/doc-creator_test.go +++ b/vdr/doc-creator_test.go @@ -19,7 +19,7 @@ import ( type mockKeyCreator struct { // jwkStr hold the predefined key in a json web key string jwkStr string - t *testing.T + t *testing.T } // New uses a predefined ECDSA key and calls the namingFunc to get the kid @@ -40,7 +40,7 @@ var jwkString = `{"crv":"P-256","kid":"did:nuts:ARRW2e42qyVjQZiACk4Up3mzpshZdJBD func TestDocCreator_Create(t *testing.T) { t.Run("ok", func(t *testing.T) { kc := &mockKeyCreator{ - t: t, + t: t, jwkStr: jwkString, } sut := NutsDocCreator{keyCreator: kc} @@ -126,4 +126,4 @@ func Test_keyToVerificationMethod(t *testing.T) { assert.Equal(t, "keyID", vm.ID.String()) assert.Equal(t, "JsonWebKey2020", vm.Type) assert.Equal(t, jwa.EllipticCurveAlgorithm("P-256"), vm.PublicKeyJwk["crv"]) -} \ No newline at end of file +} diff --git a/vdr/engine/engine.go b/vdr/engine/engine.go index 84e76774aa..da723f7bc5 100644 --- a/vdr/engine/engine.go +++ b/vdr/engine/engine.go @@ -24,6 +24,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/network" "io/ioutil" "os" "strings" @@ -38,20 +40,19 @@ import ( ) // NewVDREngine returns the core definition for the VDR -func NewVDREngine() *core.Engine { - r := vdr.Instance() - +func NewVDREngine(keyStore crypto.KeyStore, networkInstance network.Network) *core.Engine { + instance := vdr.NewVDR(vdr.DefaultConfig(), keyStore, networkInstance) return &core.Engine{ Cmd: cmd(), - Runnable: r, - Configurable: r, - Diagnosable: r, - Config: &r.Config, + Runnable: instance, + Configurable: instance, + Diagnosable: instance, + Config: &instance.Config, ConfigKey: "vdr", FlagSet: flagSet(), Name: vdr.ModuleName, Routes: func(router core.EchoRouter) { - api.RegisterHandlers(router, &api.Wrapper{VDR: r}) + api.RegisterHandlers(router, &api.Wrapper{VDR: instance}) }, } } diff --git a/vdr/engine/engine_test.go b/vdr/engine/engine_test.go index b450b1fe70..c87abecf4e 100644 --- a/vdr/engine/engine_test.go +++ b/vdr/engine/engine_test.go @@ -20,6 +20,9 @@ package engine import ( "bytes" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/network" + "github.com/nuts-foundation/nuts-node/test/io" "net/http" "net/http/httptest" "os" @@ -40,13 +43,15 @@ func Test_flagSet(t *testing.T) { } func TestNewRegistryEngine(t *testing.T) { - // Register test instance singleton + testDirectory := io.TestDirectory(t) + cryptoInstance := crypto.NewTestCryptoInstance(testDirectory) + networkInstance := network.NewTestNetworkInstance(testDirectory) t.Run("instance", func(t *testing.T) { - assert.NotNil(t, NewVDREngine()) + assert.NotNil(t, NewVDREngine(cryptoInstance, networkInstance)) }) t.Run("configuration", func(t *testing.T) { - e := NewVDREngine() + e := NewVDREngine(cryptoInstance, networkInstance) cfg := core.NutsConfig() cfg.RegisterFlags(e.Cmd, e) assert.NoError(t, cfg.InjectIntoEngine(e)) @@ -55,9 +60,11 @@ func TestNewRegistryEngine(t *testing.T) { func TestEngine_Command(t *testing.T) { core.NutsConfig().Load(&cobra.Command{}) - + testDirectory := io.TestDirectory(t) + cryptoInstance := crypto.NewTestCryptoInstance(testDirectory) + networkInstance := network.NewTestNetworkInstance(testDirectory) createCmd := func(t *testing.T) *cobra.Command { - return NewVDREngine().Cmd + return NewVDREngine(cryptoInstance, networkInstance).Cmd } exampleID, _ := did.ParseDID("did:nuts:Fx8kamg7Bom4gyEzmJc9t9QmWTkCwSxu3mrp3CbkehR7") @@ -222,4 +229,4 @@ func Test_httpClient(t *testing.T) { client := httpClient() assert.Equal(t, "http://localhost", client.ServerAddress) }) -} \ No newline at end of file +} diff --git a/vdr/network/ambassador.go b/vdr/network/ambassador.go deleted file mode 100644 index 673b650724..0000000000 --- a/vdr/network/ambassador.go +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Nuts node - * Copyright (C) 2021 Nuts community - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package network - -import ( - "bytes" - - network "github.com/nuts-foundation/nuts-network/pkg" - "github.com/nuts-foundation/nuts-network/pkg/model" - - "github.com/nuts-foundation/nuts-node/vdr/logging" -) - -const documentType = "nuts.registry-event" - -// Ambassador acts as integration point between the registry and network by sending registry events to the -// network and (later on) process notifications of new documents on the network that might be of interest to the registry. -type Ambassador interface { - // Start instructs the ambassador to start receiving events from the network. - Start() -} - -type ambassador struct { - networkClient network.NetworkClient -} - -// NewAmbassador creates a new Ambassador. Don't forget to call RegisterEventHandlers afterwards. -func NewAmbassador(networkClient network.NetworkClient) Ambassador { - instance := &ambassador{ - networkClient: networkClient, - } - return instance -} - -// Start instructs the ambassador to start receiving events from the network. -func (n *ambassador) Start() { - queue := n.networkClient.Subscribe(documentType) - go func() { - for { - document := queue.Get() - if document == nil { - return - } - n.processDocument(document) - } - }() -} - -func (n *ambassador) sendEventToNetwork() { - logging.Log().Infof("Event published on network") -} - -func (n *ambassador) processDocument(document *model.Document) { - logging.Log().Infof("Received event through Nuts Network: %s", document.Hash) - reader, err := n.networkClient.GetDocumentContents(document.Hash) - if err != nil { - logging.Log().Errorf("Unable to retrieve document from Nuts Network (hash=%s): %v", document.Hash, err) - return - } - buf := new(bytes.Buffer) - if _, err = buf.ReadFrom(reader); err != nil { - logging.Log().Errorf("Unable read document data from Nuts Network (hash=%s): %v", document.Hash, err) - return - } - // todo process -} diff --git a/vdr/network/mock.go b/vdr/network/mock.go deleted file mode 100644 index 0a565ffd39..0000000000 --- a/vdr/network/mock.go +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: pkg/network/ambassador.go - -// Package network is a generated GoMock package. -package network - -import ( - gomock "github.com/golang/mock/gomock" - reflect "reflect" -) - -// MockAmbassador is a mock of Ambassador interface -type MockAmbassador struct { - ctrl *gomock.Controller - recorder *MockAmbassadorMockRecorder -} - -// MockAmbassadorMockRecorder is the mock recorder for MockAmbassador -type MockAmbassadorMockRecorder struct { - mock *MockAmbassador -} - -// NewMockAmbassador creates a new mock instance -func NewMockAmbassador(ctrl *gomock.Controller) *MockAmbassador { - mock := &MockAmbassador{ctrl: ctrl} - mock.recorder = &MockAmbassadorMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockAmbassador) EXPECT() *MockAmbassadorMockRecorder { - return m.recorder -} - -// Start mocks base method -func (m *MockAmbassador) Start() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Start") -} - -// Start indicates an expected call of Start -func (mr *MockAmbassadorMockRecorder) Start() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockAmbassador)(nil).Start)) -} diff --git a/vdr/vdr.go b/vdr/vdr.go index 53e4407dbc..50eeafe239 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -25,6 +25,7 @@ package vdr import ( "fmt" + "github.com/nuts-foundation/nuts-node/network" "sync" "time" @@ -36,13 +37,6 @@ import ( "github.com/nuts-foundation/nuts-node/vdr/logging" - "github.com/nuts-foundation/nuts-network/pkg" - - "github.com/nuts-foundation/nuts-node/vdr/network" - - networkClient "github.com/nuts-foundation/nuts-network/client" - networkPkg "github.com/nuts-foundation/nuts-network/pkg" - "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/crypto/hash" @@ -54,9 +48,9 @@ import ( type VDR struct { Config Config store types.Store - network networkPkg.NetworkClient + network network.Network OnChange func(registry *VDR) - networkAmbassador network.Ambassador + networkAmbassador Ambassador configOnce sync.Once _logger *logrus.Entry didDocCreator types.DocCreator @@ -65,20 +59,8 @@ type VDR struct { var instance *VDR var oneVDR sync.Once -// Instance returns the singleton VDR -func Instance() *VDR { - if instance != nil { - return instance - } - oneVDR.Do(func() { - instance = NewVDR(DefaultConfig(), crypto.Instance(), networkClient.NewNetworkClient()) - }) - - return instance -} - // NewVDR creates a new VDR with provided params -func NewVDR(config Config, cryptoClient crypto.KeyStore, networkClient pkg.NetworkClient) *VDR { +func NewVDR(config Config, cryptoClient crypto.KeyStore, networkClient network.Network) *VDR { return &VDR{ Config: config, network: networkClient, @@ -96,7 +78,7 @@ func (r *VDR) Configure() error { cfg := core.NutsConfig() if cfg.Mode() == core.ServerEngineMode { if r.networkAmbassador == nil { - r.networkAmbassador = network.NewAmbassador(r.network) + r.networkAmbassador = NewAmbassador(r.network) } } })