Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add downloads endpoint API call #5

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
102 changes: 102 additions & 0 deletions message/api.go
Original file line number Diff line number Diff line change
@@ -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"`
}
62 changes: 1 addition & 61 deletions message/message.go → message/dnclient.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package message

import (
"errors"
"strings"
"time"
)
import "time"

// DNClient API message types
const (
Expand Down Expand Up @@ -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, ", "))
}
Loading