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 }