diff --git a/pkg/gcputil/firebase.go b/pkg/gcputil/firebase.go index 082ed1d..b1c2cbf 100644 --- a/pkg/gcputil/firebase.go +++ b/pkg/gcputil/firebase.go @@ -1,14 +1,19 @@ package gcputil import ( + "bytes" "context" "encoding/json" "fmt" "github.com/sirupsen/logrus" + "google.golang.org/api/option" + "google.golang.org/api/option/internaloption" "io" + "io/ioutil" "net/http" + "time" - "golang.org/x/oauth2/google" + htransport "google.golang.org/api/transport/http" ) // FirebaseDBClient is a client to interact with the Firebase Realtime Database API @@ -25,26 +30,99 @@ type DatabaseInstance struct { State string `json:"state"` } -// NewFirebaseDBClient creates a new Firebase Realtime Database client to interact with the +type loggingTransport struct { + transport http.RoundTripper +} + +func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + start := time.Now() + + // Log the request + logRequest(req) + + // Perform the request + res, err := t.transport.RoundTrip(req) + if err != nil { + fmt.Printf("Error: %v\n", err) + return nil, err + } + + // Log the response + logResponse(res, time.Since(start)) + + return res, nil +} + +func logRequest(req *http.Request) { + fmt.Printf("Request: %s %s\n", req.Method, req.URL) + fmt.Printf("Headers: %v\n", req.Header) + + if req.Body != nil { + bodyBytes, err := ioutil.ReadAll(req.Body) + if err != nil { + fmt.Printf("Error reading request body: %v\n", err) + } else { + req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) // Reassign the body + fmt.Printf("Body: %s\n", string(bodyBytes)) + } + } +} + +func logResponse(res *http.Response, duration time.Duration) { + fmt.Printf("Response: %s in %v\n", res.Status, duration) + fmt.Printf("Headers: %v\n", res.Header) + + if res.Body != nil { + bodyBytes, err := ioutil.ReadAll(res.Body) + if err != nil { + fmt.Printf("Error reading response body: %v\n", err) + } else { + res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) // Reassign the body + fmt.Printf("Body: %s\n", string(bodyBytes)) + } + } +} + +const basePath = "https://firebasedatabase.googleapis.com/" +const basePathTemplate = "https://firebasedatabase.UNIVERSE_DOMAIN/" +const mtlsBasePath = "https://firebasedatabase.mtls.googleapis.com/" + +// NewFirebaseDatabaseService creates a new Firebase Realtime Database client to interact with the // https://firebasedatabase.googleapis.com endpoints as there is no official golang client library -func NewFirebaseDBClient(ctx context.Context) (*FirebaseDBClient, error) { - client, err := google.DefaultClient(ctx, +func NewFirebaseDatabaseService(ctx context.Context, opts ...option.ClientOption) (*FirebaseDatabaseService, error) { + scopesOption := internaloption.WithDefaultScopes( "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/firebase", "https://www.googleapis.com/auth/firebase.readonly", ) + // NOTE: prepend, so we don't override user-specified scopes. + opts = append([]option.ClientOption{scopesOption}, opts...) + opts = append(opts, internaloption.WithDefaultEndpoint(basePath)) + opts = append(opts, internaloption.WithDefaultEndpointTemplate(basePathTemplate)) + opts = append(opts, internaloption.WithDefaultMTLSEndpoint(mtlsBasePath)) + opts = append(opts, internaloption.EnableNewAuthLibrary()) + client, endpoint, err := htransport.NewClient(ctx, opts...) if err != nil { - return nil, fmt.Errorf("failed to parse JWT from credentials: %v", err) + return nil, err + } + + s := &FirebaseDatabaseService{client: client, BasePath: basePath} + if endpoint != "" { + s.BasePath = endpoint } - return &FirebaseDBClient{ - httpClient: client, - }, nil + return s, nil +} + +type FirebaseDatabaseService struct { + client *http.Client + BasePath string // API endpoint base URL + UserAgent string // optional additional User-Agent fragment } // ListDatabaseRegions lists Firebase Realtime Database regions -func (c *FirebaseDBClient) ListDatabaseRegions() []string { +func (s *FirebaseDatabaseService) ListDatabaseRegions() []string { return []string{ "us-central1", "europe-west1", @@ -53,8 +131,8 @@ func (c *FirebaseDBClient) ListDatabaseRegions() []string { } // ListDatabaseInstances lists Firebase Realtime Database instances -func (c *FirebaseDBClient) ListDatabaseInstances(ctx context.Context, parent string) ([]*DatabaseInstance, error) { - url1 := fmt.Sprintf("https://firebasedatabase.googleapis.com/v1beta/%s/instances", parent) +func (s *FirebaseDatabaseService) ListDatabaseInstances(ctx context.Context, parent string) ([]*DatabaseInstance, error) { + url1 := fmt.Sprintf("%sv1beta/%s/instances", s.BasePath, parent) logrus.Tracef("url: %s", url1) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url1, nil) @@ -62,7 +140,7 @@ func (c *FirebaseDBClient) ListDatabaseInstances(ctx context.Context, parent str return nil, fmt.Errorf("error creating request: %v", err) } - resp, err := c.httpClient.Do(req) + resp, err := s.client.Do(req) if err != nil { return nil, fmt.Errorf("error requesting database instances: %v", err) } @@ -84,8 +162,8 @@ func (c *FirebaseDBClient) ListDatabaseInstances(ctx context.Context, parent str } // DeleteDatabaseInstance deletes a Firebase Realtime Database instance -func (c *FirebaseDBClient) DeleteDatabaseInstance(ctx context.Context, parent, name string) error { - url := fmt.Sprintf("https://firebasedatabase.googleapis.com/v1beta/%s/instances/%s", parent, name) +func (s *FirebaseDatabaseService) DeleteDatabaseInstance(ctx context.Context, parent, name string) error { + url := fmt.Sprintf("%sv1beta/%s/instances/%s", s.BasePath, parent, name) logrus.Tracef("url: %s", url) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) @@ -93,7 +171,7 @@ func (c *FirebaseDBClient) DeleteDatabaseInstance(ctx context.Context, parent, n return fmt.Errorf("error creating request: %v", err) } - resp, err := c.httpClient.Do(req) + resp, err := s.client.Do(req) if err != nil { return fmt.Errorf("error deleting database instance: %v", err) } @@ -107,8 +185,8 @@ func (c *FirebaseDBClient) DeleteDatabaseInstance(ctx context.Context, parent, n } // DisableDatabaseInstance disables a Firebase Realtime Database instance -func (c *FirebaseDBClient) DisableDatabaseInstance(ctx context.Context, parent, name string) error { - url := fmt.Sprintf("https://firebasedatabase.googleapis.com/v1beta/%s/instances/%s:disable", parent, name) +func (s *FirebaseDatabaseService) DisableDatabaseInstance(ctx context.Context, parent, name string) error { + url := fmt.Sprintf("%sv1beta/%s/instances/%s:disable", s.BasePath, parent, name) logrus.Tracef("url: %s", url) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) @@ -116,7 +194,7 @@ func (c *FirebaseDBClient) DisableDatabaseInstance(ctx context.Context, parent, return fmt.Errorf("error creating request: %v", err) } - resp, err := c.httpClient.Do(req) + resp, err := s.client.Do(req) if err != nil { return fmt.Errorf("error disabling database instance: %v", err) } diff --git a/resources/firebase-realtime-database.go b/resources/firebase-realtime-database.go index 0e6c942..125ab74 100644 --- a/resources/firebase-realtime-database.go +++ b/resources/firebase-realtime-database.go @@ -5,10 +5,12 @@ import ( "fmt" "slices" "strings" + "time" "github.com/gotidy/ptr" firebase "firebase.google.com/go" + liberror "github.com/ekristen/libnuke/pkg/errors" "github.com/ekristen/libnuke/pkg/registry" "github.com/ekristen/libnuke/pkg/resource" @@ -33,7 +35,7 @@ func init() { } type FirebaseRealtimeDatabaseLister struct { - svc *gcputil.FirebaseDBClient + svc *gcputil.FirebaseDatabaseService } func (l *FirebaseRealtimeDatabaseLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { @@ -46,7 +48,7 @@ func (l *FirebaseRealtimeDatabaseLister) List(ctx context.Context, o interface{} if l.svc == nil { var err error - l.svc, err = gcputil.NewFirebaseDBClient(ctx) + l.svc, err = gcputil.NewFirebaseDatabaseService(ctx) if err != nil { return nil, err } @@ -57,6 +59,9 @@ func (l *FirebaseRealtimeDatabaseLister) List(ctx context.Context, o interface{} return nil, liberror.ErrSkipRequest("region is not supported") } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + resp, err := l.svc.ListDatabaseInstances(ctx, fmt.Sprintf("projects/%s/locations/%s", *opts.Project, *opts.Region)) if err != nil { return nil, err @@ -87,7 +92,7 @@ func (l *FirebaseRealtimeDatabaseLister) List(ctx context.Context, o interface{} } type FirebaseRealtimeDatabase struct { - svc *gcputil.FirebaseDBClient + svc *gcputil.FirebaseDatabaseService settings *settings.Setting Project *string Region *string