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 new file mode 100644 index 0000000..4c77f19 --- /dev/null +++ b/message/api.go @@ -0,0 +1,102 @@ +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"` +} + +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.) + 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:"dnclient"` + Mobile string `json:"mobile"` +} + +type DNClientVersionInfo struct { + Latest bool `json:"latest"` + ReleaseDate string `json:"releaseDate"` +} 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, ", ")) -}