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  }