<commit_before>// Package rest contains the code to use the REST-based Google API
package rest

import (
	"context"
	"encoding/base64"
	"fmt"
	"strconv"
	"strings"

	"github.com/google/go-tpm-tools/launcher/verifier"

	v1alpha1 "google.golang.org/api/confidentialcomputing/v1alpha1"
	"google.golang.org/api/googleapi"
	"google.golang.org/api/option"
)

// NewClient creates a new REST client which is configured to perform
// attestations in a particular project and region.
func NewClient(ctx context.Context, projectID string, region string, opts ...option.ClientOption) (verifier.Client, error) {
	service, err := v1alpha1.NewService(ctx, opts...)
	if err != nil {
		return nil, fmt.Errorf("can't create ConfidentialComputing v1alpha1 API client: %w", err)
	}

	projectName := fmt.Sprintf("projects/%s", projectID)
	locationName := fmt.Sprintf("%s/locations/%v", projectName, region)

	location, err := service.Projects.Locations.Get(locationName).Do()
	if err == nil {
		return &restClient{service, location}, nil
	}

	// Check if the error was due to a bad region name
	if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Code == 403 {
		// In this case, inform the user about the allowed regions
		if list, listErr := service.Projects.Locations.List(projectName).Do(); listErr == nil {
			locations := make([]string, len(list.Locations))
			for i, loc := range list.Locations {
				locations[i] = loc.LocationId
			}
			return nil, fmt.Errorf(
				"unable to find region %q, available regions are [%s]: %w",
				region, strings.Join(locations, ", "), err,
			)
		}
	}

	return nil, fmt.Errorf("unable to use project %q and region %q: %w", projectID, region, err)
}

type restClient struct {
	service  *v1alpha1.Service
	location *v1alpha1.Location
}

// CreateChallenge implements verifier.Client
func (c *restClient) CreateChallenge(ctx context.Context) (*verifier.Challenge, error) {
	// Pass an empty Challenge for the input (all params are output-only)
	chal, err := c.service.Projects.Locations.Challenges.Create(
		c.location.Name,
		&v1alpha1.Challenge{},
	).Do()
	if err != nil {
		return nil, fmt.Errorf("calling v1alpha1.CreateChallenge: %w", err)
	}
	return convertChallengeFromREST(chal)
}

// VerifyAttestation implements verifier.Client
func (c *restClient) VerifyAttestation(ctx context.Context, request verifier.VerifyAttestationRequest) (*verifier.VerifyAttestationResponse, error) {
	if request.Challenge == nil || request.Attestation == nil {
		return nil, fmt.Errorf("nil value provided in challenge")
	}
	response, err := c.service.Projects.Locations.Challenges.VerifyAttestation(
		request.Challenge.Name,
		convertRequestToREST(request),<commit_msg>Improve rest.NewClient error handling. (#218)<commit_after>// Package rest contains the code to use the REST-based Google API
package rest

import (
	"context"
	"encoding/base64"
	"fmt"
	"strconv"
	"strings"

	"github.com/google/go-tpm-tools/launcher/verifier"

	v1alpha1 "google.golang.org/api/confidentialcomputing/v1alpha1"
	"google.golang.org/api/option"
)

// BadRegionError indicates that:
//   - the requested Region cannot be used with this API
//   - other Regions _can_ be used with this API
type BadRegionError struct {
	RequestedRegion  string
	AvailableRegions []string
	err              error
}

func (e *BadRegionError) Error() string {
	return fmt.Sprintf(
		"invalid region %q, available regions are [%s]: %v",
		e.RequestedRegion, strings.Join(e.AvailableRegions, ", "), e.err,
	)
}

func (e *BadRegionError) Unwrap() error {
	return e.err
}

// NewClient creates a new REST client which is configured to perform
// attestations in a particular project and region. Returns a *BadRegionError
// if the requested project is valid, but the region is invalid.
func NewClient(ctx context.Context, projectID string, region string, opts ...option.ClientOption) (verifier.Client, error) {
	service, err := v1alpha1.NewService(ctx, opts...)
	if err != nil {
		return nil, fmt.Errorf("can't create ConfidentialComputing v1alpha1 API client: %w", err)
	}

	projectName := fmt.Sprintf("projects/%s", projectID)
	locationName := fmt.Sprintf("%s/locations/%v", projectName, region)

	location, getErr := service.Projects.Locations.Get(locationName).Do()
	if getErr == nil {
		return &restClient{service, location}, nil
	}

	// If we can't get the location, try to list the locations. This handles
	// situations where the projectID is invalid.
	list, listErr := service.Projects.Locations.List(projectName).Do()
	if listErr != nil {
		return nil, fmt.Errorf("listing regions in project %q: %w", projectID, listErr)
	}

	// The project is valid, but can't get the desired region.
	regions := make([]string, len(list.Locations))
	for i, loc := range list.Locations {
		regions[i] = loc.LocationId
	}
	return nil, &BadRegionError{
		RequestedRegion:  region,
		AvailableRegions: regions,
		err:              getErr,
	}
}

type restClient struct {
	service  *v1alpha1.Service
	location *v1alpha1.Location
}

// CreateChallenge implements verifier.Client
func (c *restClient) CreateChallenge(ctx context.Context) (*verifier.Challenge, error) {
	// Pass an empty Challenge for the input (all params are output-only)
	chal, err := c.service.Projects.Locations.Challenges.Create(
		c.location.Name,
		&v1alpha1.Challenge{},
	).Do()
	if err != nil {
		return nil, fmt.Errorf("calling v1alpha1.CreateChallenge: %w", err)
	}
	return convertChallengeFromREST(chal)
}

// VerifyAttestation implements verifier.Client
func (c *restClient) VerifyAttestation(ctx context.Context, request verifier.VerifyAttestationRequest) (*verifier.VerifyAttestationResponse, error) {
	if request.Challenge == nil || request.Attestation == nil {
		return nil, fmt.Errorf("nil value provided in challenge")
	}
	response, err := c.service.Projects.Locations.Challenges.VerifyAttestation(
		request.Challenge.Name,
		convertRequestToREST(request),
{'fraction_non_alphanumeric': 0.12124809225029676, 'fraction_numerical': 0.007122265558758691, 'mean_word_length': 4.45101663585952, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '337578', 'n_tokens_mistral': 1978, 'n_tokens_neox': 1852, 'n_words': 533}