github.com/supabase/cli@v1.168.1/internal/hostnames/common.go (about) 1 package hostnames 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 9 "github.com/go-errors/errors" 10 "github.com/supabase/cli/internal/utils" 11 "github.com/supabase/cli/pkg/api" 12 ) 13 14 func GetCustomHostnameConfig(ctx context.Context, projectRef string) (*api.GetCustomHostnameConfigResponse, error) { 15 resp, err := utils.GetSupabase().GetCustomHostnameConfigWithResponse(ctx, projectRef) 16 if err != nil { 17 return nil, errors.Errorf("failed to get custom hostname: %w", err) 18 } 19 if resp.JSON200 == nil { 20 return nil, errors.New("failed to get custom hostname config; received: " + string(resp.Body)) 21 } 22 return resp, nil 23 } 24 25 func VerifyCNAME(ctx context.Context, projectRef string, customHostname string) error { 26 expectedEndpoint := fmt.Sprintf("%s.", utils.GetSupabaseHost(projectRef)) 27 cname, err := utils.ResolveCNAME(ctx, customHostname) 28 if err != nil { 29 return errors.Errorf("expected custom hostname '%s' to have a CNAME record pointing to your project at '%s', but it failed to resolve: %w", customHostname, expectedEndpoint, err) 30 } 31 if cname != expectedEndpoint { 32 return errors.Errorf("expected custom hostname '%s' to have a CNAME record pointing to your project at '%s', but it is currently set to '%s'", customHostname, expectedEndpoint, cname) 33 } 34 return nil 35 } 36 37 type RawResponse struct { 38 Result struct { 39 CustomOriginServer string `json:"custom_origin_server"` 40 OwnershipVerification struct { 41 Name string 42 Type string 43 Value string 44 } `json:"ownership_verification"` 45 Ssl struct { 46 ValidationRecords []struct { 47 Status string `json:"status"` 48 TxtName string `json:"txt_name"` 49 TxtValue string `json:"txt_value"` 50 } `json:"validation_records"` 51 ValidationErrors []struct { 52 Message string `json:"message"` 53 } `json:"validation_errors"` 54 Status string `json:"status"` 55 } 56 } `json:"result"` 57 } 58 59 func serializeRawOutput(response *api.UpdateCustomHostnameResponse) (string, error) { 60 output, err := json.MarshalIndent(response, "", " ") 61 if err != nil { 62 return "", errors.Errorf("failed to serialize json: %w", err) 63 } 64 return string(output), nil 65 } 66 67 func appendRawOutputIfNeeded(status string, response *api.UpdateCustomHostnameResponse, includeRawOutput bool) string { 68 if !includeRawOutput { 69 return status 70 } 71 rawOutput, err := serializeRawOutput(response) 72 if err != nil { 73 return fmt.Sprintf("%s\nFailed to serialize raw output: %+v\n", status, err) 74 } 75 return fmt.Sprintf("%s\nRaw output follows:\n%s\n", status, rawOutput) 76 } 77 78 func TranslateStatus(response *api.UpdateCustomHostnameResponse, includeRawOutput bool) (string, error) { 79 if response.Status == api.N5ServicesReconfigured { 80 return appendRawOutputIfNeeded(fmt.Sprintf("Custom hostname setup completed. Project is now accessible at %s.", response.CustomHostname), response, includeRawOutput), nil 81 } 82 if response.Status == api.N4OriginSetupCompleted { 83 var res RawResponse 84 rawBody, err := json.Marshal(response.Data) 85 if err != nil { 86 return "", errors.Errorf("failed to serialize body: %w", err) 87 } 88 err = json.Unmarshal(rawBody, &res) 89 if err != nil { 90 return "", errors.Errorf("failed to deserialize body: %w", err) 91 } 92 return appendRawOutputIfNeeded(fmt.Sprintf(`Custom hostname configuration complete, and ready for activation. 93 94 Please ensure that your custom domain is set up as a CNAME record to your Supabase subdomain: 95 %s CNAME -> %s`, response.CustomHostname, res.Result.CustomOriginServer), response, includeRawOutput), nil 96 } 97 if response.Status == api.N2Initiated { 98 var res RawResponse 99 rawBody, err := json.Marshal(response.Data) 100 if err != nil { 101 return "", errors.Errorf("failed to serialize body: %w", err) 102 } 103 err = json.Unmarshal(rawBody, &res) 104 if err != nil { 105 return "", errors.Errorf("failed to deserialize body: %w", err) 106 } 107 owner := res.Result.OwnershipVerification 108 ssl := res.Result.Ssl.ValidationRecords 109 if res.Result.Ssl.Status == "initializing" { 110 return appendRawOutputIfNeeded("Custom hostname setup is being initialized; please request re-verification in a few seconds.\n", response, includeRawOutput), nil 111 } 112 if len(res.Result.Ssl.ValidationErrors) > 0 { 113 var errorMessages []string 114 for _, valError := range res.Result.Ssl.ValidationErrors { 115 if strings.Contains(valError.Message, "caa_error") { 116 return appendRawOutputIfNeeded("CAA mismatch; please remove any existing CAA records on your domain, or add one for \"digicert.com\"\n", response, includeRawOutput), nil 117 } 118 errorMessages = append(errorMessages, valError.Message) 119 } 120 valErrors := strings.Join(errorMessages, "\n\t- ") 121 return appendRawOutputIfNeeded(fmt.Sprintf("SSL validation errors: \n\t- %s\n", valErrors), response, includeRawOutput), nil 122 } 123 if len(ssl) != 1 { 124 return "", errors.Errorf("expected a single SSL verification record, received: %+v", ssl) 125 } 126 records := "" 127 if owner.Name != "" { 128 records = fmt.Sprintf("\n\t%s TXT -> %s", owner.Name, owner.Value) 129 } 130 if ssl[0].TxtName != "" { 131 records = fmt.Sprintf("%s\n\t%s TXT -> %s (replace any existing CNAME records)", records, ssl[0].TxtName, ssl[0].TxtValue) 132 } 133 status := fmt.Sprintf("Custom hostname verification in-progress; please configure the appropriate DNS entries and request re-verification.\n"+ 134 "Required outstanding validation records: %s\n", 135 records) 136 return appendRawOutputIfNeeded(status, response, includeRawOutput), nil 137 } 138 return appendRawOutputIfNeeded("Custom hostname configuration not started.", response, includeRawOutput), nil 139 }