From 2ab2ee934902494689a82587faf4a7cbea7d268e Mon Sep 17 00:00:00 2001 From: John Maguire Date: Thu, 3 Aug 2023 19:27:18 -0400 Subject: [PATCH 1/3] Split message package into two files --- message/api.go | 63 +++++++++++++++++++++++++++++ message/{message.go => dnclient.go} | 62 +--------------------------- 2 files changed, 64 insertions(+), 61 deletions(-) create mode 100644 message/api.go rename message/{message.go => dnclient.go} (58%) diff --git a/message/api.go b/message/api.go new file mode 100644 index 0000000..6113aa4 --- /dev/null +++ b/message/api.go @@ -0,0 +1,63 @@ +package message + +import ( + "errors" + "strings" + "time" +) + +// EnrollEndpoint is the REST enrollment endpoint. +const EnrollEndpoint = "/v2/enroll" + +// APIError represents a single error returned in an API error response. +type APIError struct { + Code string `json:"code"` + Message string `json:"message"` + Path string `json:"path"` // may or may not be present +} + +type APIErrors []APIError + +func (errs APIErrors) ToError() error { + if len(errs) == 0 { + return nil + } + + s := make([]string, len(errs)) + for i := range errs { + s[i] = errs[i].Message + } + + return errors.New(strings.Join(s, ", ")) +} + +// EnrollRequest is issued to the EnrollEndpoint. +type EnrollRequest struct { + Code string `json:"code"` + DHPubkey []byte `json:"dhPubkey"` + EdPubkey []byte `json:"edPubkey"` + Timestamp time.Time `json:"timestamp"` +} + +// EnrollResponse represents a response from the enrollment endpoint. +type EnrollResponse struct { + // Only one of Data or Errors should be set in a response + Data EnrollResponseData `json:"data"` + + Errors APIErrors `json:"errors"` +} + +// EnrollResponseData is included in the EnrollResponse. +type EnrollResponseData struct { + Config []byte `json:"config"` + HostID string `json:"hostID"` + Counter uint `json:"counter"` + TrustedKeys []byte `json:"trustedKeys"` + Organization EnrollResponseDataOrg `json:"organization"` +} + +// EnrollResponseDataOrg is included in EnrollResponseData. +type EnrollResponseDataOrg struct { + ID string `json:"id"` + Name string `json:"name"` +} diff --git a/message/message.go b/message/dnclient.go similarity index 58% rename from message/message.go rename to message/dnclient.go index 232e3a3..b7e1e5a 100644 --- a/message/message.go +++ b/message/dnclient.go @@ -1,10 +1,6 @@ package message -import ( - "errors" - "strings" - "time" -) +import "time" // DNClient API message types const ( @@ -72,59 +68,3 @@ type DoUpdateResponse struct { Nonce []byte `json:"nonce"` TrustedKeys []byte `json:"trustedKeys"` } - -// EnrollEndpoint is the REST enrollment endpoint. -const EnrollEndpoint = "/v2/enroll" - -// EnrollRequest is issued to the EnrollEndpoint. -type EnrollRequest struct { - Code string `json:"code"` - DHPubkey []byte `json:"dhPubkey"` - EdPubkey []byte `json:"edPubkey"` - Timestamp time.Time `json:"timestamp"` -} - -// EnrollResponse represents a response from the enrollment endpoint. -type EnrollResponse struct { - // Only one of Data or Errors should be set in a response - Data EnrollResponseData `json:"data"` - - Errors APIErrors `json:"errors"` -} - -// EnrollResponseData is included in the EnrollResponse. -type EnrollResponseData struct { - Config []byte `json:"config"` - HostID string `json:"hostID"` - Counter uint `json:"counter"` - TrustedKeys []byte `json:"trustedKeys"` - Organization EnrollResponseDataOrg `json:"organization"` -} - -// EnrollResponseDataOrg is included in EnrollResponseData. -type EnrollResponseDataOrg struct { - ID string `json:"id"` - Name string `json:"name"` -} - -// APIError represents a single error returned in an API error response. -type APIError struct { - Code string `json:"code"` - Message string `json:"message"` - Path string `json:"path"` // may or may not be present -} - -type APIErrors []APIError - -func (errs APIErrors) ToError() error { - if len(errs) == 0 { - return nil - } - - s := make([]string, len(errs)) - for i := range errs { - s[i] = errs[i].Message - } - - return errors.New(strings.Join(s, ", ")) -} From 2924c87d0d489c0502259583a9b5c49b154d64f8 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Thu, 3 Aug 2023 19:27:37 -0400 Subject: [PATCH 2/3] Add downloads endpoint messages --- message/api.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/message/api.go b/message/api.go index 6113aa4..366e4dc 100644 --- a/message/api.go +++ b/message/api.go @@ -61,3 +61,35 @@ type EnrollResponseDataOrg struct { ID string `json:"id"` Name string `json:"name"` } + +type DownloadsResponse struct { + // DNClient maps versions to a map of platforms' download links. + DNClient map[string]map[string]string `json:"dnclient"` + // Mobile maps platforms to their download links (i.e. App Store / Play Store.) + Mobile DownloadsResponseMobile `json:"mobile"` + + // VersionInfo contains information about past versions. + VersionInfo DownloadsResponseVersionInfo `json:"versionInfo"` +} + +type DownloadsResponseVersionInfo struct { + // DNClient maps versions to their version info. + DNClient map[string]DNClientVersionInfo `json:"dnclient"` + // Latest returns the latest versions for each platform. + Latest DownloadsResponseLatest `json:"latest"` +} + +type DownloadsResponseMobile struct { + Android string `json:"android"` + IOS string `json:"ios"` +} + +type DownloadsResponseLatest struct { + DNClient string `json:"string"` + Mobile string `json:"mobile"` +} + +type DNClientVersionInfo struct { + Latest bool `json:"latest"` + ReleaseDate string `json:"releaseDate"` +} From 39232b06bc3faf9dc500af690be3b6253e0c6018 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 8 Aug 2023 11:02:30 -0400 Subject: [PATCH 3/3] Add downloads endpoint API call --- client.go | 38 ++++++++++++++++++++++++++++++++++++++ message/api.go | 9 ++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index c8101b7..c8dc0a3 100644 --- a/client.go +++ b/client.go @@ -251,6 +251,44 @@ func (c *Client) DoUpdate(ctx context.Context, creds Credentials) ([]byte, []byt return result.Config, dhPrivkeyPEM, newCreds, nil } +// DetermineLatestVersion returns the latest version information. +func (c *Client) DetermineLatestVersion(ctx context.Context, logger logrus.FieldLogger) (*message.DownloadsResponseLatest, error) { + logger.WithFields(logrus.Fields{"server": c.dnServer}).Debug("Finding latest DNClient version") + + req, err := http.NewRequestWithContext(ctx, "GET", c.dnServer+message.EnrollEndpoint, nil) + if err != nil { + return nil, err + } + + resp, err := c.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Log the request ID returned from the server + reqID := resp.Header.Get("X-Request-ID") + logger.WithFields(logrus.Fields{"reqID": reqID}).Debug("Request for latest DNClient version complete") + + // Decode the response + r := message.DownloadsResponse{} + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, &APIError{e: fmt.Errorf("error reading response body: %s", err), ReqID: reqID} + } + + if err := json.Unmarshal(b, &r); err != nil { + return nil, &APIError{e: fmt.Errorf("error decoding JSON response: %s\nbody: %s", err, b), ReqID: reqID} + } + + // Check for any errors returned by the API + if err := r.Errors.ToError(); err != nil { + return nil, &APIError{e: fmt.Errorf("unexpected error during downloads fetch: %v", err), ReqID: reqID} + } + + return &r.Data.VersionInfo.Latest, nil +} + // postDNClient wraps and signs the given dnclientRequestWrapper message, and makes the API call. // On success, it returns the response message body. On error, the error is returned. func (c *Client) postDNClient(ctx context.Context, reqType string, value []byte, hostID string, counter uint, privkey ed25519.PrivateKey) ([]byte, error) { diff --git a/message/api.go b/message/api.go index 366e4dc..4c77f19 100644 --- a/message/api.go +++ b/message/api.go @@ -63,6 +63,13 @@ type EnrollResponseDataOrg struct { } type DownloadsResponse struct { + // Only one of Data or Errors should be set in a response + Data DownloadsResponseData `json:"data"` + + Errors APIErrors `json:"errors"` +} + +type DownloadsResponseData struct { // DNClient maps versions to a map of platforms' download links. DNClient map[string]map[string]string `json:"dnclient"` // Mobile maps platforms to their download links (i.e. App Store / Play Store.) @@ -85,7 +92,7 @@ type DownloadsResponseMobile struct { } type DownloadsResponseLatest struct { - DNClient string `json:"string"` + DNClient string `json:"dnclient"` Mobile string `json:"mobile"` }