commit 91d4f69a9b14ccfd3bbdf470b3ba5924bdee4ec8 Author: Jan Dittberner Date: Tue Nov 29 16:23:16 2022 +0100 Initial signer client implementation This commit adds the project setup and implements a basic signer client that sends health check commands to the signer. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..da32d53 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.go text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e30653 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*Pty +/.idea/ +/config.yaml +/dist/ +/signerclient \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..0d80f87 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,77 @@ +--- +run: + skip-files: + - internal/config/amd64.go + - internal/config/arm64.go + - internal/config/armhf.go + - pkg/messages/resolver.msgpackgen.go + +output: + sort-results: true + +linters-settings: + cyclop: + max-complexity: 15 + goheader: + values: + const: + ORGANIZATION: CAcert Inc. + template: |- + Copyright {{ YEAR-RANGE }} {{ ORGANIZATION }} + SPDX-License-Identifier: Apache-2.0 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + gomnd: + ignored-functions: + - 'strconv.*' + ignored-numbers: ["2", "16", "128", "0o600", "0o700"] + goimports: + local-prefixes: code.cacert.org,git.cacert.org + misspell: + locale: US + ignore-words: + - CAcert + +linters: + disable-all: false + enable: + - bodyclose + - containedctx + - contextcheck + - cyclop + - decorder + - errorlint + - exportloopref + - forbidigo + - forcetypeassert + - gocognit + - goconst + - gocritic + - gofmt + - goheader + - goimports + - gomnd + - gosec + - lll + - makezero + - misspell + - nakedret + - nestif + - nlreturn + - nolintlint + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - wrapcheck + - wsl diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..76adbf0 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +GOFILES := $(shell find -type f -name '*.go') +BUILD_TIME := $(shell date --rfc-3339=seconds) +COMMIT := $(shell git show-ref --head --abbrev=8 HEAD|cut -d ' ' -f 1) +VERSION := $(shell git describe --always --dirty) + +all: lint test signerclient + +lint: + golangci-lint run + +test: + go test -race ./... + +signerclient: $(GOFILES) + go build -race -ldflags="-X 'main.date=$(BUILD_TIME)' -X 'main.commit=$(COMMIT)' -X 'main.version=$(VERSION)'" ./cmd/signerclient + +clean: + rm -f signerclient + +.PHONY: test lint all clean \ No newline at end of file diff --git a/cmd/signerclient/main.go b/cmd/signerclient/main.go new file mode 100644 index 0000000..5a72f4c --- /dev/null +++ b/cmd/signerclient/main.go @@ -0,0 +1,98 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "flag" + "os" + + "github.com/sirupsen/logrus" + + "git.cacert.org/cacert-gosigner/pkg/protocol" + "git.cacert.org/cacert-gosignerclient/internal/client" + "git.cacert.org/cacert-gosignerclient/internal/config" + "git.cacert.org/cacert-gosignerclient/internal/handler" +) + +var ( + commit = "dev" + date = "unknown" + version = "unknown" +) + +const ( + defaultConfigFile = "config.yaml" +) + +func main() { + var ( + showVersion, verbose bool + configFile, logLevel string + logger *logrus.Logger + ) + + logger = logrus.New() + logger.SetOutput(os.Stdout) + logger.SetLevel(logrus.InfoLevel) + + logger.Infof("cacert-gosignerclient %s (%s) - build %s", version, commit, date) + + flag.StringVar(&configFile, "config", defaultConfigFile, "signer client configuration file") + flag.BoolVar(&showVersion, "version", false, "show version") + flag.BoolVar(&verbose, "verbose", false, "verbose output") + flag.StringVar(&logLevel, "loglevel", "INFO", "log level") + + flag.Parse() + + if showVersion { + return + } + + parsedLevel, err := logrus.ParseLevel(logLevel) + if err != nil { + logger.WithError(err).Fatal("could not parse log level") + } + + logger.SetLevel(parsedLevel) + + clientConfig, err := config.New(configFile) + if err != nil { + logger.WithError(err).Fatal("could not configure client") + } + + commands := make(chan *protocol.Command) + + clientHandler, err := handler.New(clientConfig, logger, commands) + if err != nil { + logger.WithError(err).Fatal("could not setup client handler") + } + + signerClient, err := client.New(clientConfig, logger, clientHandler, commands) + if err != nil { + logger.WithError(err).Fatal("could not setup client") + } + + defer func() { _ = signerClient.Close() }() + + logger.Info("setup complete, starting client operation") + + if err = signerClient.Run(context.Background()); err != nil { + logger.WithError(err).Fatal("error in client") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fc924f5 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module git.cacert.org/cacert-gosignerclient + +go 1.17 + +require ( + git.cacert.org/cacert-gosigner v0.0.0-20221129130510-af40662c7d61 + github.com/sirupsen/logrus v1.9.0 + github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/dave/jennifer v1.4.1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/justincpresley/go-cobs v1.2.0 // indirect + github.com/shamaton/msgpack/v2 v2.1.0 // indirect + github.com/shamaton/msgpackgen v0.3.0 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cb02d30 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +git.cacert.org/cacert-gosigner v0.0.0-20221129130510-af40662c7d61 h1:kUML0ocTKcCQeazZc40Df3GYIr00gxJrYal0arxG+tQ= +git.cacert.org/cacert-gosigner v0.0.0-20221129130510-af40662c7d61/go.mod h1:yFBRxW6BwOyiXGtHQH/Vpro4Dxd0oIl3tCIWg99nYGE= +github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= +github.com/dave/jennifer v1.4.1 h1:XyqG6cn5RQsTj3qlWQTKlRGAyrTcsk1kUmWdZBzRjDw= +github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= +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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/justincpresley/go-cobs v1.2.0 h1:dyWszWzXObEv8sxVMJTAIo9XT7HEM10vkAOZq2eVEsQ= +github.com/justincpresley/go-cobs v1.2.0/go.mod h1:L0d+EbGirv6IzsXNzwULduI2/z3ijkkAmsAuPMpLfqA= +github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shamaton/msgpack/v2 v2.1.0 h1:9jJ2eGZw2Wa9KExPX3KaDDckVjgr4zhXGFCfWagUWqg= +github.com/shamaton/msgpack/v2 v2.1.0/go.mod h1:aTUEmh31ziGX1Ml7wMPLVY0f4vT3CRsCvZRoSCs+VGg= +github.com/shamaton/msgpackgen v0.3.0 h1:q6o7prOEJFdF9BAPgkOtfzJbs55pQi7g44RUnEVUxtM= +github.com/shamaton/msgpackgen v0.3.0/go.mod h1:fd99fDDuxuTiWzkHC59uEGzrt/WDu+ltGZTbEWwVXIc= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..928b2dd --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,166 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + "fmt" + "time" + + "github.com/sirupsen/logrus" + "github.com/tarm/serial" + + "git.cacert.org/cacert-gosigner/pkg/messages" + "git.cacert.org/cacert-gosigner/pkg/protocol" + "git.cacert.org/cacert-gosignerclient/internal/config" +) + +type Client struct { + port *serial.Port + logger *logrus.Logger + framer protocol.Framer + in chan []byte + out chan []byte + commands chan *protocol.Command + handler protocol.ClientHandler + config *config.ClientConfig +} + +func (c *Client) Run(ctx context.Context) error { + protocolErrors := make(chan error) + framerErrors := make(chan error) + + go func(f protocol.Framer) { + framerErrors <- f.ReadFrames(c.port, c.in) + }(c.framer) + + go func(f protocol.Framer) { + framerErrors <- f.WriteFrames(c.port, c.out) + }(c.framer) + + go func() { + clientProtocol := protocol.NewClient(c.handler, c.commands, c.in, c.out, c.logger) + + protocolErrors <- clientProtocol.Handle() + }() + + ctx, cancelCommandLoop := context.WithCancel(ctx) + + go c.commandLoop(ctx) + + for { + select { + case <-ctx.Done(): + cancelCommandLoop() + + return nil + case err := <-framerErrors: + cancelCommandLoop() + + if err != nil { + return fmt.Errorf("error from framer: %w", err) + } + + return nil + case err := <-protocolErrors: + cancelCommandLoop() + + if err != nil { + return fmt.Errorf("error from protocol: %w", err) + } + + return nil + } + } +} + +func (c *Client) setupConnection(serialConfig *serial.Config) error { + s, err := serial.OpenPort(serialConfig) + if err != nil { + return fmt.Errorf("could not open serial port: %w", err) + } + + c.port = s + + return nil +} + +func (c *Client) Close() error { + close(c.in) + close(c.out) + + if c.port != nil { + err := c.port.Close() + if err != nil { + return fmt.Errorf("could not close serial port: %w", err) + } + } + + return nil +} + +func (c *Client) commandLoop(ctx context.Context) { + healthTimer := time.NewTimer(c.config.HealthStart) + + for { + select { + case <-ctx.Done(): + return + case <-healthTimer.C: + announce, err := messages.BuildCommandAnnounce(messages.CmdHealth) + if err != nil { + c.logger.WithError(err).Error("could not build health command announce") + } + + c.commands <- &protocol.Command{ + Announce: announce, + Command: &messages.HealthCommand{}, + } + + healthTimer.Reset(c.config.HealthInterval) + } + } +} + +func New( + cfg *config.ClientConfig, + logger *logrus.Logger, + handler protocol.ClientHandler, + commands chan *protocol.Command, +) (*Client, error) { + client := &Client{ + logger: logger, + framer: protocol.NewCOBSFramer(logger), + in: make(chan []byte), + out: make(chan []byte), + commands: commands, + handler: handler, + config: cfg, + } + + err := client.setupConnection(&serial.Config{ + Name: cfg.Serial.Device, + Baud: cfg.Serial.Baud, + ReadTimeout: cfg.Serial.Timeout, + }) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..a7c0daa --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,141 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "fmt" + "io" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +const ( + defaultSerialTimeout = 5 * time.Second + defaultHealthInterval = 30 * time.Second + defaultHealthStart = 5 * time.Second + defaultResponseAnnounceTimeout = 30 * time.Second + defaultResponseDataTimeout = 2 * time.Second +) + +type SettingsError struct { + msg string +} + +func (se SettingsError) Error() string { + return fmt.Sprintf("invalid Settings %s", se.msg) +} + +type Serial struct { + Device string `yaml:"device"` + Baud int `yaml:"baud"` + Timeout time.Duration `yaml:"timeout"` +} + +type ClientConfig struct { + Serial Serial `yaml:"serial"` + HealthInterval time.Duration `yaml:"health-interval"` + HealthStart time.Duration `yaml:"health-start"` + ResponseAnnounceTimeout time.Duration `yaml:"response-announce-timeout"` + ResponseDataTimeout time.Duration `yaml:"response-data-timeout"` +} + +func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error { + data := struct { + Serial Serial `yaml:"serial"` + HealthInterval time.Duration `yaml:"health-interval"` + HealthStart time.Duration `yaml:"health-start"` + ResponseAnnounceTimeout time.Duration `yaml:"response-announce-timeout"` + ResponseDataTimeout time.Duration `yaml:"response-data-timeout"` + }{} + + err := n.Decode(&data) + if err != nil { + return fmt.Errorf("could not decode YAML: %w", err) + } + + if data.Serial.Device == "" { + return SettingsError{"you must specify a serial 'device'"} + } + + if data.Serial.Baud == 0 { + data.Serial.Baud = 115200 + } + + if data.Serial.Timeout == 0 { + data.Serial.Timeout = defaultSerialTimeout + } + + c.Serial = data.Serial + + if data.HealthInterval == 0 { + data.HealthInterval = defaultHealthInterval + } + + c.HealthInterval = data.HealthInterval + + if data.HealthStart == 0 { + data.HealthStart = defaultHealthStart + } + + c.HealthStart = data.HealthStart + + if data.ResponseAnnounceTimeout == 0 { + data.ResponseAnnounceTimeout = defaultResponseAnnounceTimeout + } + + c.ResponseAnnounceTimeout = data.ResponseAnnounceTimeout + + if data.ResponseDataTimeout == 0 { + data.ResponseDataTimeout = defaultResponseDataTimeout + } + + c.ResponseDataTimeout = data.ResponseDataTimeout + + return nil +} + +func New(clientConfigFile string) (*ClientConfig, error) { + configFile, err := os.Open(clientConfigFile) + if err != nil { + return nil, fmt.Errorf("could not open signer client configuration file %s: %w", clientConfigFile, err) + } + + defer func() { _ = configFile.Close() }() + + clientConfig, err := loadConfiguration(configFile) + if err != nil { + return nil, fmt.Errorf("could not load client configuration from %s: %w", clientConfigFile, err) + } + + return clientConfig, nil +} + +func loadConfiguration(r io.Reader) (*ClientConfig, error) { + var config *ClientConfig + + decoder := yaml.NewDecoder(r) + err := decoder.Decode(&config) + + if err != nil { + return nil, fmt.Errorf("could not parse YAML configuration: %w", err) + } + + return config, nil +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go new file mode 100644 index 0000000..cce5707 --- /dev/null +++ b/internal/handler/handler.go @@ -0,0 +1,136 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package handler + +import ( + "fmt" + "time" + + "github.com/shamaton/msgpackgen/msgpack" + "github.com/sirupsen/logrus" + + "git.cacert.org/cacert-gosigner/pkg/messages" + + "git.cacert.org/cacert-gosigner/pkg/protocol" + "git.cacert.org/cacert-gosignerclient/internal/config" +) + +type SignerClientHandler struct { + logger *logrus.Logger + commands chan *protocol.Command + config *config.ClientConfig +} + +func (s *SignerClientHandler) Send(command *protocol.Command, out chan []byte) error { + var ( + frame []byte + err error + ) + + frame, err = msgpack.Marshal(command.Announce) + if err != nil { + return fmt.Errorf("could not marshal command annoucement: %w", err) + } + + s.logger.WithField("announcement", command.Announce).Info("write command announcement") + + s.logger.Trace("writing command announcement") + + out <- frame + + frame, err = msgpack.Marshal(command.Command) + if err != nil { + return fmt.Errorf("could not marshal command data: %w", err) + } + + s.logger.WithField("command", command.Command).Info("write command data") + + out <- frame + + return nil +} + +func (s *SignerClientHandler) ResponseAnnounce(in chan []byte) (*protocol.Response, error) { + response := &protocol.Response{} + + var announce messages.ResponseAnnounce + + select { + case frame := <-in: + if err := msgpack.Unmarshal(frame, &announce); err != nil { + return nil, fmt.Errorf("could not unmarshal response announcement: %w", err) + } + + response.Announce = &announce + + s.logger.WithField("announcement", response.Announce).Debug("received response announcement") + + return response, nil + case <-time.After(s.config.ResponseAnnounceTimeout): + return nil, protocol.ErrResponseAnnounceTimeoutExpired + } +} + +func (s *SignerClientHandler) ResponseData(in chan []byte, response *protocol.Response) error { + select { + case frame := <-in: + switch response.Announce.Code { + case messages.RespHealth: + var resp messages.HealthResponse + if err := msgpack.Unmarshal(frame, &resp); err != nil { + return fmt.Errorf("could not unmarshal health response data: %w", err) + } + + response.Response = &resp + case messages.RespFetchCRL: + var resp messages.FetchCRLResponse + if err := msgpack.Unmarshal(frame, &resp); err != nil { + return fmt.Errorf("could not unmarshal fetch CRL response data: %w", err) + } + + response.Response = &resp + default: + return fmt.Errorf("unhandled response code %s", response.Announce.Code) + } + case <-time.After(s.config.ResponseDataTimeout): + return protocol.ErrResponseDataTimeoutExpired + } + + return nil +} + +func (s *SignerClientHandler) HandleResponse(response *protocol.Response) error { + s.logger.WithField("response", response.Announce).Info("handled response") + s.logger.WithField("response", response).Debug("full response") + + // TODO: add real implementations + + return nil +} + +func New( + config *config.ClientConfig, + logger *logrus.Logger, + commands chan *protocol.Command, +) (protocol.ClientHandler, error) { + return &SignerClientHandler{ + logger: logger, + config: config, + commands: commands, + }, nil +}