github.com/confluentinc/confluent-kafka-go@v1.9.2/schemaregistry/rest_service.go (about)

     1  /**
     2   * Copyright 2022 Confluent Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   * http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package schemaregistry
    18  
    19  import (
    20  	"bytes"
    21  	"crypto/tls"
    22  	"encoding/base64"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"log"
    28  	"net"
    29  	"net/http"
    30  	"net/url"
    31  	"strings"
    32  	"time"
    33  )
    34  
    35  // Relative Confluent Schema Registry REST API endpoints as described in the Confluent documentation
    36  // https://docs.confluent.io/current/schema-registry/docs/api.html
    37  const (
    38  	base              = ".."
    39  	schemas           = "/schemas/ids/%d"
    40  	schemasBySubject  = "/schemas/ids/%d?subject=%s"
    41  	subject           = "/subjects"
    42  	subjects          = subject + "/%s"
    43  	subjectsNormalize = subject + "/%s?normalize=%t"
    44  	subjectsDelete    = subjects + "?permanent=%t"
    45  	version           = subjects + "/versions"
    46  	versionNormalize  = subjects + "/versions?normalize=%t"
    47  	versions          = version + "/%v"
    48  	versionsDelete    = versions + "?permanent=%t"
    49  	compatibility     = "/compatibility" + versions
    50  	config            = "/config"
    51  	subjectConfig     = config + "/%s"
    52  	mode              = "/mode"
    53  	modeConfig        = mode + "/%s"
    54  )
    55  
    56  // REST API request
    57  type api struct {
    58  	method    string
    59  	endpoint  string
    60  	arguments []interface{}
    61  	body      interface{}
    62  }
    63  
    64  // newRequest returns new Confluent Schema Registry API request */
    65  func newRequest(method string, endpoint string, body interface{}, arguments ...interface{}) *api {
    66  	return &api{
    67  		method:    method,
    68  		endpoint:  endpoint,
    69  		arguments: arguments,
    70  		body:      body,
    71  	}
    72  }
    73  
    74  /*
    75  * HTTP error codes/ SR int:error_code:
    76  *	402: Invalid {resource}
    77  *	404: {resource} not found
    78  *		- 40401 - Subject not found
    79  *		- 40402 - SchemaMetadata not found
    80  *		- 40403 - Schema not found
    81  *	422: Invalid {resource}
    82  *		- 42201 - Invalid Schema
    83  *		- 42202 - Invalid SchemaMetadata
    84  *	500: Internal Server Error (something broke between SR and Kafka)
    85  *		- 50001 - Error in backend(kafka)
    86  *		- 50002 - Operation timed out
    87  *		- 50003 - Error forwarding request to SR leader
    88   */
    89  
    90  // RestError represents a Schema Registry HTTP Error response
    91  type RestError struct {
    92  	Code    int    `json:"error_code"`
    93  	Message string `json:"message"`
    94  }
    95  
    96  // Error implements the errors.Error interface
    97  func (err *RestError) Error() string {
    98  	return fmt.Sprintf("schema registry request failed error code: %d: %s", err.Code, err.Message)
    99  }
   100  
   101  type restService struct {
   102  	url     *url.URL
   103  	headers http.Header
   104  	*http.Client
   105  }
   106  
   107  // newRestService returns a new REST client for the Confluent Schema Registry
   108  func newRestService(conf *Config) (*restService, error) {
   109  	urlConf := conf.SchemaRegistryURL
   110  	u, err := url.Parse(urlConf)
   111  
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	headers, err := newAuthHeader(u, conf)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	headers.Add("Content-Type", "application/vnd.schemaregistry.v1+json")
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	transport, err := configureTransport(conf)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	timeout := conf.RequestTimeoutMs
   132  
   133  	return &restService{
   134  		url:     u,
   135  		headers: headers,
   136  		Client: &http.Client{
   137  			Transport: transport,
   138  			Timeout:   time.Duration(timeout) * time.Millisecond,
   139  		},
   140  	}, nil
   141  }
   142  
   143  // encodeBasicAuth adds a basic http authentication header to the provided header
   144  func encodeBasicAuth(userinfo string) string {
   145  	return base64.StdEncoding.EncodeToString([]byte(userinfo))
   146  }
   147  
   148  // configureTLS populates tlsConf
   149  func configureTLS(conf *Config, tlsConf *tls.Config) error {
   150  	certFile := conf.SslCertificateLocation
   151  	keyFile := conf.SslKeyLocation
   152  	caFile := conf.SslCaLocation
   153  	unsafe := conf.SslDisableEndpointVerification
   154  
   155  	var err error
   156  	if certFile != "" {
   157  		var cert tls.Certificate
   158  		cert, err := tls.LoadX509KeyPair(certFile, keyFile)
   159  		if err != nil {
   160  			return err
   161  		}
   162  		tlsConf.Certificates = []tls.Certificate{cert}
   163  	}
   164  
   165  	if caFile != "" {
   166  		if unsafe {
   167  			log.Println("WARN: endpoint verification is currently disabled. " +
   168  				"This feature should be configured for development purposes only")
   169  		}
   170  		var caCert []byte
   171  		caCert, err := ioutil.ReadFile(caFile)
   172  		if err != nil {
   173  			return err
   174  		}
   175  		tlsConf.RootCAs.AppendCertsFromPEM(caCert)
   176  		if err != nil {
   177  			return err
   178  		}
   179  	}
   180  
   181  	tlsConf.BuildNameToCertificate()
   182  
   183  	return err
   184  }
   185  
   186  // configureTransport returns a new Transport for use by the Confluent Schema Registry REST client
   187  func configureTransport(conf *Config) (*http.Transport, error) {
   188  
   189  	// Exposed for testing purposes only. In production properly formed certificates should be used
   190  	// https://tools.ietf.org/html/rfc2818#section-3
   191  	tlsConfig := &tls.Config{}
   192  	if err := configureTLS(conf, tlsConfig); err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	timeout := conf.ConnectionTimeoutMs
   197  
   198  	return &http.Transport{
   199  		Dial: (&net.Dialer{
   200  			Timeout: time.Duration(timeout) * time.Millisecond,
   201  		}).Dial,
   202  		TLSClientConfig: tlsConfig,
   203  	}, nil
   204  }
   205  
   206  // configureURLAuth copies the url userinfo into a basic HTTP auth authorization header
   207  func configureURLAuth(service *url.URL, header http.Header) error {
   208  	header.Add("Authorization", fmt.Sprintf("Basic %s", encodeBasicAuth(service.User.String())))
   209  	return nil
   210  }
   211  
   212  // configureSASLAuth copies the sasl username and password into a HTTP basic authorization header
   213  func configureSASLAuth(conf *Config, header http.Header) error {
   214  	mech := conf.SaslMechanism
   215  	if strings.ToUpper(mech) == "GSSAPI" {
   216  		return fmt.Errorf("SASL_INHERIT support PLAIN and SCRAM SASL mechanisms only")
   217  	}
   218  
   219  	user := conf.SaslUsername
   220  	pass := conf.SaslPassword
   221  	if user == "" || pass == "" {
   222  		return fmt.Errorf("SASL_INHERIT requires both sasl.username and sasl.password be set")
   223  	}
   224  
   225  	header.Add("Authorization", fmt.Sprintf("Basic %s", encodeBasicAuth(fmt.Sprintf("%s:%s", user, pass))))
   226  	return nil
   227  }
   228  
   229  // configureUSERINFOAuth copies basic.auth.user.info
   230  func configureUSERINFOAuth(conf *Config, header http.Header) error {
   231  	auth := conf.BasicAuthUserInfo
   232  	if auth == "" {
   233  		return fmt.Errorf("USER_INFO source configured without basic.auth.user.info ")
   234  	}
   235  
   236  	header.Add("Authorization", fmt.Sprintf("Basic %s", encodeBasicAuth(auth)))
   237  	return nil
   238  
   239  }
   240  
   241  // newAuthHeader returns a base64 encoded userinfo string identified on the configured credentials source
   242  func newAuthHeader(service *url.URL, conf *Config) (http.Header, error) {
   243  	// Remove userinfo from url regardless of source to avoid confusion/conflicts
   244  	defer func() {
   245  		service.User = nil
   246  	}()
   247  
   248  	source := conf.BasicAuthCredentialsSource
   249  
   250  	header := http.Header{}
   251  
   252  	var err error
   253  	switch strings.ToUpper(source) {
   254  	case "URL":
   255  		err = configureURLAuth(service, header)
   256  	case "SASL_INHERIT":
   257  		err = configureSASLAuth(conf, header)
   258  	case "USER_INFO":
   259  		err = configureUSERINFOAuth(conf, header)
   260  	default:
   261  		err = fmt.Errorf("unrecognized value for basic.auth.credentials.source %s", source)
   262  	}
   263  	return header, err
   264  }
   265  
   266  // handleRequest sends a HTTP(S) request to the Schema Registry, placing results into the response object
   267  func (rs *restService) handleRequest(request *api, response interface{}) error {
   268  	endpoint, err := rs.url.Parse(fmt.Sprintf(base+request.endpoint, request.arguments...))
   269  	if err != nil {
   270  		return err
   271  	}
   272  
   273  	var readCloser io.ReadCloser
   274  	if request.body != nil {
   275  		outbuf, err := json.Marshal(request.body)
   276  		if err != nil {
   277  			return err
   278  		}
   279  		readCloser = ioutil.NopCloser(bytes.NewBuffer(outbuf))
   280  	}
   281  
   282  	req := &http.Request{
   283  		Method: request.method,
   284  		URL:    endpoint,
   285  		Body:   readCloser,
   286  		Header: rs.headers,
   287  	}
   288  
   289  	resp, err := rs.Do(req)
   290  
   291  	if err != nil {
   292  		return err
   293  	}
   294  
   295  	defer resp.Body.Close()
   296  	if resp.StatusCode == 200 {
   297  		if err = json.NewDecoder(resp.Body).Decode(response); err != nil {
   298  			return err
   299  		}
   300  		return nil
   301  	}
   302  
   303  	var failure RestError
   304  	if err := json.NewDecoder(resp.Body).Decode(&failure); err != nil {
   305  		return err
   306  	}
   307  
   308  	return &failure
   309  }