github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/http/backend.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package http 7 8 import ( 9 "context" 10 "crypto/tls" 11 "crypto/x509" 12 "errors" 13 "fmt" 14 "log" 15 "net/http" 16 "net/url" 17 "regexp" 18 "strings" 19 "time" 20 21 "github.com/hashicorp/go-retryablehttp" 22 23 "github.com/opentofu/opentofu/internal/backend" 24 "github.com/opentofu/opentofu/internal/encryption" 25 "github.com/opentofu/opentofu/internal/legacy/helper/schema" 26 "github.com/opentofu/opentofu/internal/logging" 27 "github.com/opentofu/opentofu/internal/states/remote" 28 "github.com/opentofu/opentofu/internal/states/statemgr" 29 ) 30 31 func New(enc encryption.StateEncryption) backend.Backend { 32 s := &schema.Backend{ 33 Schema: map[string]*schema.Schema{ 34 "address": &schema.Schema{ 35 Type: schema.TypeString, 36 Required: true, 37 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_ADDRESS", nil), 38 Description: "The address of the REST endpoint", 39 }, 40 "update_method": &schema.Schema{ 41 Type: schema.TypeString, 42 Optional: true, 43 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UPDATE_METHOD", "POST"), 44 Description: "HTTP method to use when updating state", 45 }, 46 "lock_address": &schema.Schema{ 47 Type: schema.TypeString, 48 Optional: true, 49 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_ADDRESS", nil), 50 Description: "The address of the lock REST endpoint", 51 }, 52 "unlock_address": &schema.Schema{ 53 Type: schema.TypeString, 54 Optional: true, 55 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_ADDRESS", nil), 56 Description: "The address of the unlock REST endpoint", 57 }, 58 "lock_method": &schema.Schema{ 59 Type: schema.TypeString, 60 Optional: true, 61 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_METHOD", "LOCK"), 62 Description: "The HTTP method to use when locking", 63 }, 64 "unlock_method": &schema.Schema{ 65 Type: schema.TypeString, 66 Optional: true, 67 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_METHOD", "UNLOCK"), 68 Description: "The HTTP method to use when unlocking", 69 }, 70 "username": &schema.Schema{ 71 Type: schema.TypeString, 72 Optional: true, 73 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_USERNAME", nil), 74 Description: "The username for HTTP basic authentication", 75 }, 76 "password": &schema.Schema{ 77 Type: schema.TypeString, 78 Optional: true, 79 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_PASSWORD", nil), 80 Description: "The password for HTTP basic authentication", 81 }, 82 "skip_cert_verification": &schema.Schema{ 83 Type: schema.TypeBool, 84 Optional: true, 85 Default: false, 86 Description: "Whether to skip TLS verification.", 87 }, 88 "retry_max": &schema.Schema{ 89 Type: schema.TypeInt, 90 Optional: true, 91 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_MAX", 2), 92 Description: "The number of HTTP request retries.", 93 }, 94 "retry_wait_min": &schema.Schema{ 95 Type: schema.TypeInt, 96 Optional: true, 97 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MIN", 1), 98 Description: "The minimum time in seconds to wait between HTTP request attempts.", 99 }, 100 "retry_wait_max": &schema.Schema{ 101 Type: schema.TypeInt, 102 Optional: true, 103 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MAX", 30), 104 Description: "The maximum time in seconds to wait between HTTP request attempts.", 105 }, 106 "client_ca_certificate_pem": &schema.Schema{ 107 Type: schema.TypeString, 108 Optional: true, 109 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CA_CERTIFICATE_PEM", ""), 110 Description: "A PEM-encoded CA certificate chain used by the client to verify server certificates during TLS authentication.", 111 }, 112 "client_certificate_pem": &schema.Schema{ 113 Type: schema.TypeString, 114 Optional: true, 115 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CERTIFICATE_PEM", ""), 116 Description: "A PEM-encoded certificate used by the server to verify the client during mutual TLS (mTLS) authentication.", 117 }, 118 "client_private_key_pem": &schema.Schema{ 119 Type: schema.TypeString, 120 Optional: true, 121 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_PRIVATE_KEY_PEM", ""), 122 Description: "A PEM-encoded private key, required if client_certificate_pem is specified.", 123 }, 124 "headers": &schema.Schema{ 125 Type: schema.TypeMap, 126 Elem: &schema.Schema{Type: schema.TypeString}, 127 Optional: true, 128 ValidateFunc: func(cv interface{}, ck string) ([]string, []error) { 129 nameRegex := regexp.MustCompile("[^a-zA-Z0-9-_]") 130 valueRegex := regexp.MustCompile("[^[:ascii:]]") 131 132 headers := cv.(map[string]interface{}) 133 err := make([]error, 0, len(headers)) 134 for name, value := range headers { 135 if len(name) == 0 || nameRegex.MatchString(name) { 136 err = append(err, fmt.Errorf( 137 "%s \"%s\" name must not be empty and only contain A-Za-z0-9-_ characters", ck, name)) 138 } 139 140 v := value.(string) 141 if len(strings.TrimSpace(v)) == 0 || valueRegex.MatchString(v) { 142 err = append(err, fmt.Errorf( 143 "%s \"%s\" value must not be empty and only contain ascii characters", ck, name)) 144 } 145 } 146 return nil, err 147 }, 148 Description: "A map of headers, when set will be included with HTTP requests sent to the HTTP backend", 149 }, 150 }, 151 } 152 153 b := &Backend{Backend: s, encryption: enc} 154 b.Backend.ConfigureFunc = b.configure 155 return b 156 } 157 158 type Backend struct { 159 *schema.Backend 160 encryption encryption.StateEncryption 161 162 client *httpClient 163 } 164 165 // configureTLS configures TLS when needed; if there are no conditions requiring TLS, no change is made. 166 func (b *Backend) configureTLS(client *retryablehttp.Client, data *schema.ResourceData) error { 167 // If there are no conditions needing to configure TLS, leave the client untouched 168 skipCertVerification := data.Get("skip_cert_verification").(bool) 169 clientCACertificatePem := data.Get("client_ca_certificate_pem").(string) 170 clientCertificatePem := data.Get("client_certificate_pem").(string) 171 clientPrivateKeyPem := data.Get("client_private_key_pem").(string) 172 if !skipCertVerification && clientCACertificatePem == "" && clientCertificatePem == "" && clientPrivateKeyPem == "" { 173 return nil 174 } 175 if clientCertificatePem != "" && clientPrivateKeyPem == "" { 176 return fmt.Errorf("client_certificate_pem is set but client_private_key_pem is not") 177 } 178 if clientPrivateKeyPem != "" && clientCertificatePem == "" { 179 return fmt.Errorf("client_private_key_pem is set but client_certificate_pem is not") 180 } 181 182 // TLS configuration is needed; create an object and configure it 183 var tlsConfig tls.Config 184 client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tlsConfig 185 186 if skipCertVerification { 187 // ignores TLS verification 188 tlsConfig.InsecureSkipVerify = true 189 } 190 if clientCACertificatePem != "" { 191 // trust servers based on a CA 192 tlsConfig.RootCAs = x509.NewCertPool() 193 if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(clientCACertificatePem)) { 194 return errors.New("failed to append certs") 195 } 196 } 197 if clientCertificatePem != "" && clientPrivateKeyPem != "" { 198 // attach a client certificate to the TLS handshake (aka mTLS) 199 certificate, err := tls.X509KeyPair([]byte(clientCertificatePem), []byte(clientPrivateKeyPem)) 200 if err != nil { 201 return fmt.Errorf("cannot load client certificate: %w", err) 202 } 203 tlsConfig.Certificates = []tls.Certificate{certificate} 204 } 205 206 return nil 207 } 208 209 func (b *Backend) configure(ctx context.Context) error { 210 data := schema.FromContextBackendConfig(ctx) 211 212 address := data.Get("address").(string) 213 updateURL, err := url.Parse(address) 214 if err != nil { 215 return fmt.Errorf("failed to parse address URL: %w", err) 216 } 217 if updateURL.Scheme != "http" && updateURL.Scheme != "https" { 218 return fmt.Errorf("address must be HTTP or HTTPS") 219 } 220 221 updateMethod := data.Get("update_method").(string) 222 223 var lockURL *url.URL 224 if v, ok := data.GetOk("lock_address"); ok && v.(string) != "" { 225 var err error 226 lockURL, err = url.Parse(v.(string)) 227 if err != nil { 228 return fmt.Errorf("failed to parse lockAddress URL: %w", err) 229 } 230 if lockURL.Scheme != "http" && lockURL.Scheme != "https" { 231 return fmt.Errorf("lockAddress must be HTTP or HTTPS") 232 } 233 } 234 235 lockMethod := data.Get("lock_method").(string) 236 237 var unlockURL *url.URL 238 if v, ok := data.GetOk("unlock_address"); ok && v.(string) != "" { 239 var err error 240 unlockURL, err = url.Parse(v.(string)) 241 if err != nil { 242 return fmt.Errorf("failed to parse unlockAddress URL: %w", err) 243 } 244 if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" { 245 return fmt.Errorf("unlockAddress must be HTTP or HTTPS") 246 } 247 } 248 249 unlockMethod := data.Get("unlock_method").(string) 250 251 username := data.Get("username").(string) 252 password := data.Get("password").(string) 253 254 var headers map[string]string 255 if dv, ok := data.GetOk("headers"); ok { 256 dh := dv.(map[string]interface{}) 257 headers = make(map[string]string, len(dh)) 258 259 for k, v := range dh { 260 switch strings.ToLower(k) { 261 case "authorization": 262 if username != "" { 263 return fmt.Errorf("headers \"%s\" cannot be set when providing username", k) 264 } 265 case "content-type", "content-md5": 266 return fmt.Errorf("headers \"%s\" is reserved", k) 267 default: 268 headers[k] = v.(string) 269 } 270 } 271 } 272 273 rClient := retryablehttp.NewClient() 274 rClient.RetryMax = data.Get("retry_max").(int) 275 rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second 276 rClient.RetryWaitMax = time.Duration(data.Get("retry_wait_max").(int)) * time.Second 277 rClient.Logger = log.New(logging.LogOutput(), "", log.Flags()) 278 if err = b.configureTLS(rClient, data); err != nil { 279 return err 280 } 281 282 b.client = &httpClient{ 283 URL: updateURL, 284 UpdateMethod: updateMethod, 285 286 LockURL: lockURL, 287 LockMethod: lockMethod, 288 UnlockURL: unlockURL, 289 UnlockMethod: unlockMethod, 290 291 Headers: headers, 292 Username: username, 293 Password: password, 294 295 // accessible only for testing use 296 Client: rClient, 297 } 298 return nil 299 } 300 301 func (b *Backend) StateMgr(name string) (statemgr.Full, error) { 302 if name != backend.DefaultStateName { 303 return nil, backend.ErrWorkspacesNotSupported 304 } 305 306 return remote.NewState(b.client, b.encryption), nil 307 } 308 309 func (b *Backend) Workspaces() ([]string, error) { 310 return nil, backend.ErrWorkspacesNotSupported 311 } 312 313 func (b *Backend) DeleteWorkspace(string, bool) error { 314 return backend.ErrWorkspacesNotSupported 315 }