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 }