github.com/supabase/cli@v1.168.1/internal/utils/api.go (about)

     1  package utils
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"encoding/json"
     7  	"fmt"
     8  	"log"
     9  	"net"
    10  	"net/http"
    11  	"net/http/httptrace"
    12  	"net/textproto"
    13  	"sync"
    14  
    15  	"github.com/go-errors/errors"
    16  	"github.com/spf13/viper"
    17  	"github.com/supabase/cli/internal/utils/cloudflare"
    18  	supabase "github.com/supabase/cli/pkg/api"
    19  )
    20  
    21  const (
    22  	DNS_GO_NATIVE  = "native"
    23  	DNS_OVER_HTTPS = "https"
    24  )
    25  
    26  var (
    27  	clientOnce sync.Once
    28  	apiClient  *supabase.ClientWithResponses
    29  
    30  	DNSResolver = EnumFlag{
    31  		Allowed: []string{DNS_GO_NATIVE, DNS_OVER_HTTPS},
    32  		Value:   DNS_GO_NATIVE,
    33  	}
    34  )
    35  
    36  // Performs DNS lookup via HTTPS, in case firewall blocks native netgo resolver.
    37  func FallbackLookupIP(ctx context.Context, host string) ([]string, error) {
    38  	if net.ParseIP(host) != nil {
    39  		return []string{host}, nil
    40  	}
    41  	// Ref: https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json
    42  	cf := cloudflare.NewCloudflareAPI()
    43  	data, err := cf.DNSQuery(ctx, cloudflare.DNSParams{Name: host})
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	// Look for first valid IP
    48  	var resolved []string
    49  	for _, answer := range data.Answer {
    50  		if answer.Type == cloudflare.TypeA || answer.Type == cloudflare.TypeAAAA {
    51  			resolved = append(resolved, answer.Data)
    52  		}
    53  	}
    54  	if len(resolved) == 0 {
    55  		return nil, errors.Errorf("failed to locate valid IP for %s; resolves to %#v", host, data.Answer)
    56  	}
    57  	return resolved, nil
    58  }
    59  
    60  func ResolveCNAME(ctx context.Context, host string) (string, error) {
    61  	// Ref: https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json
    62  	cf := cloudflare.NewCloudflareAPI()
    63  	data, err := cf.DNSQuery(ctx, cloudflare.DNSParams{Name: host, Type: Ptr(cloudflare.TypeCNAME)})
    64  	if err != nil {
    65  		return "", err
    66  	}
    67  	// Look for first valid IP
    68  	for _, answer := range data.Answer {
    69  		if answer.Type == cloudflare.TypeCNAME {
    70  			return answer.Data, nil
    71  		}
    72  	}
    73  	serialized, err := json.MarshalIndent(data.Answer, "", "    ")
    74  	if err != nil {
    75  		// we ignore the error (not great), and use the underlying struct in our error message
    76  		return "", errors.Errorf("failed to locate appropriate CNAME record for %s; resolves to %+v", host, data.Answer)
    77  	}
    78  	return "", errors.Errorf("failed to locate appropriate CNAME record for %s; resolves to %+v", host, serialized)
    79  }
    80  
    81  func WithTraceContext(ctx context.Context) context.Context {
    82  	trace := &httptrace.ClientTrace{
    83  		DNSStart: func(info httptrace.DNSStartInfo) {
    84  			log.Printf("DNS Start: %+v\n", info)
    85  		},
    86  		DNSDone: func(info httptrace.DNSDoneInfo) {
    87  			if info.Err != nil {
    88  				log.Println("DNS Error:", info.Err)
    89  			} else {
    90  				log.Printf("DNS Done: %+v\n", info)
    91  			}
    92  		},
    93  		ConnectStart: func(network, addr string) {
    94  			log.Println("Connect Start:", network, addr)
    95  		},
    96  		ConnectDone: func(network, addr string, err error) {
    97  			if err != nil {
    98  				log.Println("Connect Error:", network, addr, err)
    99  			} else {
   100  				log.Println("Connect Done:", network, addr)
   101  			}
   102  		},
   103  		TLSHandshakeStart: func() {
   104  			log.Println("TLS Start")
   105  		},
   106  		TLSHandshakeDone: func(cs tls.ConnectionState, err error) {
   107  			if err != nil {
   108  				log.Println("TLS Error:", err)
   109  			} else {
   110  				log.Printf("TLS Done: %+v\n", cs)
   111  			}
   112  		},
   113  		WroteHeaderField: func(key string, value []string) {
   114  			log.Println("Sent Header:", key, value)
   115  		},
   116  		WroteRequest: func(wr httptrace.WroteRequestInfo) {
   117  			if wr.Err != nil {
   118  				log.Println("Send Error:", wr.Err)
   119  			} else {
   120  				log.Println("Send Done")
   121  			}
   122  		},
   123  		Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
   124  			log.Println("Recv 1xx:", code, header)
   125  			return nil
   126  		},
   127  		GotFirstResponseByte: func() {
   128  			log.Println("Recv First Byte")
   129  		},
   130  	}
   131  	return httptrace.WithClientTrace(ctx, trace)
   132  }
   133  
   134  type DialContextFunc func(context.Context, string, string) (net.Conn, error)
   135  
   136  // Wraps a DialContext with DNS-over-HTTPS as fallback resolver
   137  func withFallbackDNS(dialContext DialContextFunc) DialContextFunc {
   138  	dnsOverHttps := func(ctx context.Context, network, address string) (net.Conn, error) {
   139  		host, port, err := net.SplitHostPort(address)
   140  		if err != nil {
   141  			return nil, errors.Errorf("failed to split host port: %w", err)
   142  		}
   143  		ip, err := FallbackLookupIP(ctx, host)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  		conn, err := dialContext(ctx, network, net.JoinHostPort(ip[0], port))
   148  		if err != nil {
   149  			return nil, errors.Errorf("failed to dial fallback: %w", err)
   150  		}
   151  		return conn, nil
   152  	}
   153  	if DNSResolver.Value == DNS_OVER_HTTPS {
   154  		return dnsOverHttps
   155  	}
   156  	nativeWithFallback := func(ctx context.Context, network, address string) (net.Conn, error) {
   157  		conn, err := dialContext(ctx, network, address)
   158  		// Workaround when pure Go DNS resolver fails https://github.com/golang/go/issues/12524
   159  		if err, ok := err.(net.Error); ok && err.Timeout() {
   160  			if conn, err := dnsOverHttps(ctx, network, address); err == nil {
   161  				return conn, nil
   162  			}
   163  		}
   164  		if err != nil {
   165  			return nil, errors.Errorf("failed to dial native: %w", err)
   166  		}
   167  		return conn, nil
   168  	}
   169  	return nativeWithFallback
   170  }
   171  
   172  func GetSupabase() *supabase.ClientWithResponses {
   173  	clientOnce.Do(func() {
   174  		token, err := LoadAccessToken()
   175  		if err != nil {
   176  			log.Fatalln(err)
   177  		}
   178  		if t, ok := http.DefaultTransport.(*http.Transport); ok {
   179  			t.DialContext = withFallbackDNS(t.DialContext)
   180  		}
   181  		apiClient, err = supabase.NewClientWithResponses(
   182  			GetSupabaseAPIHost(),
   183  			supabase.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
   184  				req.Header.Set("Authorization", "Bearer "+token)
   185  				req.Header.Set("User-Agent", "SupabaseCLI/"+Version)
   186  				return nil
   187  			}),
   188  		)
   189  		if err != nil {
   190  			log.Fatalln(err)
   191  		}
   192  	})
   193  	return apiClient
   194  }
   195  
   196  const (
   197  	DefaultApiHost = "https://api.supabase.com"
   198  	// DEPRECATED
   199  	DeprecatedApiHost = "https://api.supabase.io"
   200  )
   201  
   202  var RegionMap = map[string]string{
   203  	"ap-northeast-1": "Northeast Asia (Tokyo)",
   204  	"ap-northeast-2": "Northeast Asia (Seoul)",
   205  	"ap-south-1":     "South Asia (Mumbai)",
   206  	"ap-southeast-1": "Southeast Asia (Singapore)",
   207  	"ap-southeast-2": "Oceania (Sydney)",
   208  	"ca-central-1":   "Canada (Central)",
   209  	"eu-central-1":   "Central EU (Frankfurt)",
   210  	"eu-west-1":      "West EU (Ireland)",
   211  	"eu-west-2":      "West EU (London)",
   212  	"eu-west-3":      "West EU (Paris)",
   213  	"sa-east-1":      "South America (São Paulo)",
   214  	"us-east-1":      "East US (North Virginia)",
   215  	"us-west-1":      "West US (North California)",
   216  	"us-west-2":      "West US (Oregon)",
   217  }
   218  
   219  var FlyRegions = map[string]string{
   220  	"ams": "Amsterdam, Netherlands",
   221  	"arn": "Stockholm, Sweden",
   222  	"bog": "Bogotá, Colombia",
   223  	"bos": "Boston, Massachusetts (US)",
   224  	"cdg": "Paris, France",
   225  	"den": "Denver, Colorado (US)",
   226  	"dfw": "Dallas, Texas (US",
   227  	"ewr": "Secaucus, NJ (US)",
   228  	"fra": "Frankfurt, Germany",
   229  	"gdl": "Guadalajara, Mexico",
   230  	"gig": "Rio de Janeiro, Brazil",
   231  	"gru": "Sao Paulo, Brazil",
   232  	"hkg": "Hong Kong, Hong Kong",
   233  	"iad": "Ashburn, Virginia (US",
   234  	"jnb": "Johannesburg, South Africa",
   235  	"lax": "Los Angeles, California (US",
   236  	"lhr": "London, United Kingdom",
   237  	"maa": "Chennai (Madras), India",
   238  	"mad": "Madrid, Spain",
   239  	"mia": "Miami, Florida (US)",
   240  	"nrt": "Tokyo, Japan",
   241  	"ord": "Chicago, Illinois (US",
   242  	"otp": "Bucharest, Romania",
   243  	"qro": "Querétaro, Mexico",
   244  	"scl": "Santiago, Chile",
   245  	"sea": "Seattle, Washington (US",
   246  	"sin": "Singapore, Singapore",
   247  	"sjc": "San Jose, California (US",
   248  	"syd": "Sydney, Australia",
   249  	"waw": "Warsaw, Poland",
   250  	"yul": "Montreal, Canada",
   251  	"yyz": "Toronto, Canada",
   252  }
   253  
   254  func GetSupabaseAPIHost() string {
   255  	apiHost := viper.GetString("INTERNAL_API_HOST")
   256  	if apiHost == "" {
   257  		apiHost = DefaultApiHost
   258  	}
   259  	return apiHost
   260  }
   261  
   262  func GetSupabaseDashboardURL() string {
   263  	switch GetSupabaseAPIHost() {
   264  	case DefaultApiHost, DeprecatedApiHost:
   265  		return "https://supabase.com/dashboard"
   266  	case "https://api.supabase.green":
   267  		return "https://app.supabase.green"
   268  	default:
   269  		return "http://127.0.0.1:8082"
   270  	}
   271  }
   272  
   273  func GetSupabaseDbHost(projectRef string) string {
   274  	// TODO: query projects api for db_host
   275  	switch GetSupabaseAPIHost() {
   276  	case DefaultApiHost, DeprecatedApiHost:
   277  		return fmt.Sprintf("db.%s.supabase.co", projectRef)
   278  	case "https://api.supabase.green":
   279  		return fmt.Sprintf("db.%s.supabase.red", projectRef)
   280  	default:
   281  		return fmt.Sprintf("db.%s.supabase.red", projectRef)
   282  	}
   283  }
   284  
   285  func GetSupabaseHost(projectRef string) string {
   286  	switch GetSupabaseAPIHost() {
   287  	case DefaultApiHost, DeprecatedApiHost:
   288  		return fmt.Sprintf("%s.supabase.co", projectRef)
   289  	case "https://api.supabase.green":
   290  		return fmt.Sprintf("%s.supabase.red", projectRef)
   291  	default:
   292  		return fmt.Sprintf("%s.supabase.red", projectRef)
   293  	}
   294  }