github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/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 "time" 15 16 supabase "github.com/Redstoneguy129/cli/pkg/api" 17 "github.com/deepmap/oapi-codegen/pkg/securityprovider" 18 "github.com/spf13/viper" 19 ) 20 21 var ( 22 clientOnce sync.Once 23 apiClient *supabase.ClientWithResponses 24 httpClient = http.Client{Timeout: 10 * time.Second} 25 ) 26 27 const ( 28 // Ref: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 29 dnsIPv4Type uint16 = 1 30 cnameType uint16 = 5 31 dnsIPv6Type uint16 = 28 32 ) 33 34 type dnsAnswer struct { 35 Type uint16 `json:"type"` 36 Data string `json:"data"` 37 } 38 39 type dnsResponse struct { 40 Answer []dnsAnswer `json:",omitempty"` 41 } 42 43 // Performs DNS lookup via HTTPS, in case firewall blocks native netgo resolver. 44 func fallbackLookupIP(ctx context.Context, address string) string { 45 host, port, err := net.SplitHostPort(address) 46 if err != nil { 47 return "" 48 } 49 // Ref: https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json 50 req, err := http.NewRequestWithContext(ctx, "GET", "https://1.1.1.1/dns-query?name="+host, nil) 51 if err != nil { 52 return "" 53 } 54 req.Header.Add("accept", "application/dns-json") 55 // Sends request 56 resp, err := httpClient.Do(req) 57 if err != nil { 58 return "" 59 } 60 defer resp.Body.Close() 61 if resp.StatusCode != http.StatusOK { 62 return "" 63 } 64 // Parses response 65 var data dnsResponse 66 dec := json.NewDecoder(resp.Body) 67 if err := dec.Decode(&data); err != nil { 68 return "" 69 } 70 // Look for first valid IP 71 for _, answer := range data.Answer { 72 if answer.Type == dnsIPv4Type || answer.Type == dnsIPv6Type { 73 return net.JoinHostPort(answer.Data, port) 74 } 75 } 76 return "" 77 } 78 79 func ResolveCNAME(ctx context.Context, host string) (string, error) { 80 // Ref: https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json 81 req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://1.1.1.1/dns-query?name=%s&type=CNAME", host), nil) 82 if err != nil { 83 return "", fmt.Errorf("failed to initialize request: %w", err) 84 } 85 req.Header.Add("accept", "application/dns-json") 86 // Sends request 87 resp, err := httpClient.Do(req) 88 if err != nil { 89 return "", fmt.Errorf("failed to execute resolution request: %w", err) 90 } 91 defer resp.Body.Close() 92 if resp.StatusCode != http.StatusOK { 93 return "", fmt.Errorf("resolution response code was not 200: %s", resp.Status) 94 } 95 // Parses response 96 var data dnsResponse 97 dec := json.NewDecoder(resp.Body) 98 if err := dec.Decode(&data); err != nil { 99 return "", fmt.Errorf("failed to decode response: %w", err) 100 } 101 // Look for first valid IP 102 for _, answer := range data.Answer { 103 if answer.Type == cnameType { 104 return answer.Data, nil 105 } 106 } 107 serialized, err := json.MarshalIndent(data.Answer, "", " ") 108 if err != nil { 109 // we ignore the error (not great), and use the underlying struct in our error message 110 return "", fmt.Errorf("failed to locate appropriate CNAME record for %s; resolves to %+v", host, data.Answer) 111 } 112 return "", fmt.Errorf("failed to locate appropriate CNAME record for %s; resolves to %+v", host, serialized) 113 } 114 115 func WithTraceContext(ctx context.Context) context.Context { 116 trace := &httptrace.ClientTrace{ 117 DNSStart: func(info httptrace.DNSStartInfo) { 118 log.Printf("DNS Start: %+v\n", info) 119 }, 120 DNSDone: func(info httptrace.DNSDoneInfo) { 121 if info.Err != nil { 122 log.Println("DNS Error:", info.Err) 123 } else { 124 log.Printf("DNS Done: %+v\n", info) 125 } 126 }, 127 ConnectStart: func(network, addr string) { 128 log.Println("Connect Start:", network, addr) 129 }, 130 ConnectDone: func(network, addr string, err error) { 131 if err != nil { 132 log.Println("Connect Error:", network, addr, err) 133 } else { 134 log.Println("Connect Done:", network, addr) 135 } 136 }, 137 TLSHandshakeStart: func() { 138 log.Println("TLS Start") 139 }, 140 TLSHandshakeDone: func(cs tls.ConnectionState, err error) { 141 if err != nil { 142 log.Println("TLS Error:", err) 143 } else { 144 log.Printf("TLS Done: %+v\n", cs) 145 } 146 }, 147 WroteHeaderField: func(key string, value []string) { 148 log.Println("Sent Header:", key, value) 149 }, 150 WroteRequest: func(wr httptrace.WroteRequestInfo) { 151 if wr.Err != nil { 152 log.Println("Send Error:", wr.Err) 153 } else { 154 log.Println("Send Done") 155 } 156 }, 157 Got1xxResponse: func(code int, header textproto.MIMEHeader) error { 158 log.Println("Recv 1xx:", code, header) 159 return nil 160 }, 161 GotFirstResponseByte: func() { 162 log.Println("Recv First Byte") 163 }, 164 } 165 return httptrace.WithClientTrace(ctx, trace) 166 } 167 168 func GetSupabase() *supabase.ClientWithResponses { 169 clientOnce.Do(func() { 170 token, err := LoadAccessToken() 171 if err != nil { 172 log.Fatalln(err) 173 } 174 provider, err := securityprovider.NewSecurityProviderBearerToken(token) 175 if err != nil { 176 log.Fatalln(err) 177 } 178 if t, ok := http.DefaultTransport.(*http.Transport); ok { 179 dialContext := t.DialContext 180 t.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { 181 conn, err := dialContext(ctx, network, address) 182 // Workaround when pure Go DNS resolver fails https://github.com/golang/go/issues/12524 183 if err, ok := err.(*net.OpError); ok && err.Op == "dial" { 184 if ip := fallbackLookupIP(ctx, address); ip != "" { 185 return dialContext(ctx, network, ip) 186 } 187 } 188 return conn, err 189 } 190 } 191 apiClient, err = supabase.NewClientWithResponses( 192 GetSupabaseAPIHost(), 193 supabase.WithRequestEditorFn(provider.Intercept), 194 ) 195 if err != nil { 196 log.Fatalln(err) 197 } 198 }) 199 return apiClient 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 } 217 218 func GetSupabaseAPIHost() string { 219 apiHost := viper.GetString("INTERNAL_API_HOST") 220 if apiHost == "" { 221 apiHost = "https://api.supabase.io" 222 } 223 return apiHost 224 } 225 226 func GetSupabaseDashboardURL() string { 227 switch GetSupabaseAPIHost() { 228 case "https://api.supabase.com", "https://api.supabase.io": 229 return "https://app.supabase.com" 230 case "https://api.supabase.green": 231 return "https://app.supabase.green" 232 default: 233 return "http://localhost:8082" 234 } 235 } 236 237 func GetSupabaseDbHost(projectRef string) string { 238 // TODO: query projects api for db_host 239 switch GetSupabaseAPIHost() { 240 case "https://api.supabase.com", "https://api.supabase.io": 241 return fmt.Sprintf("db.%s.supabase.co", projectRef) 242 case "https://api.supabase.green": 243 return fmt.Sprintf("db.%s.supabase.red", projectRef) 244 default: 245 return fmt.Sprintf("db.%s.supabase.red", projectRef) 246 } 247 } 248 249 func GetSupabaseHost(projectRef string) string { 250 switch GetSupabaseAPIHost() { 251 case "https://api.supabase.com", "https://api.supabase.io": 252 return fmt.Sprintf("%s.supabase.co", projectRef) 253 case "https://api.supabase.green": 254 return fmt.Sprintf("%s.supabase.red", projectRef) 255 default: 256 return fmt.Sprintf("%s.supabase.red", projectRef) 257 } 258 }