blob: c222d949bcb4d9273df53757b5d06d93ad6d2d88 [file] [log] [blame]
Serge Bazanskicc25bdf2018-10-25 14:02:58 +02001// Copyright 2015 go-swagger maintainers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package client
16
17import (
18 "context"
19 "crypto"
20 "crypto/ecdsa"
21 "crypto/rsa"
22 "crypto/tls"
23 "crypto/x509"
24 "encoding/pem"
25 "fmt"
26 "io/ioutil"
27 "mime"
28 "net/http"
29 "net/http/httputil"
30 "strings"
31 "sync"
32 "time"
33
34 "github.com/go-openapi/runtime"
35 "github.com/go-openapi/runtime/logger"
36 "github.com/go-openapi/runtime/middleware"
37 "github.com/go-openapi/strfmt"
38)
39
40// TLSClientOptions to configure client authentication with mutual TLS
41type TLSClientOptions struct {
42 // Certificate is the path to a PEM-encoded certificate to be used for
43 // client authentication. If set then Key must also be set.
44 Certificate string
45
46 // LoadedCertificate is the certificate to be used for client authentication.
47 // This field is ignored if Certificate is set. If this field is set, LoadedKey
48 // is also required.
49 LoadedCertificate *x509.Certificate
50
51 // Key is the path to an unencrypted PEM-encoded private key for client
52 // authentication. This field is required if Certificate is set.
53 Key string
54
55 // LoadedKey is the key for client authentication. This field is required if
56 // LoadedCertificate is set.
57 LoadedKey crypto.PrivateKey
58
59 // CA is a path to a PEM-encoded certificate that specifies the root certificate
60 // to use when validating the TLS certificate presented by the server. If this field
61 // (and LoadedCA) is not set, the system certificate pool is used. This field is ignored if LoadedCA
62 // is set.
63 CA string
64
65 // LoadedCA specifies the root certificate to use when validating the server's TLS certificate.
66 // If this field (and CA) is not set, the system certificate pool is used.
67 LoadedCA *x509.Certificate
68
69 // ServerName specifies the hostname to use when verifying the server certificate.
70 // If this field is set then InsecureSkipVerify will be ignored and treated as
71 // false.
72 ServerName string
73
74 // InsecureSkipVerify controls whether the certificate chain and hostname presented
75 // by the server are validated. If false, any certificate is accepted.
76 InsecureSkipVerify bool
77
78 // Prevents callers using unkeyed fields.
79 _ struct{}
80}
81
82// TLSClientAuth creates a tls.Config for mutual auth
83func TLSClientAuth(opts TLSClientOptions) (*tls.Config, error) {
84 // create client tls config
85 cfg := &tls.Config{}
86
87 // load client cert if specified
88 if opts.Certificate != "" {
89 cert, err := tls.LoadX509KeyPair(opts.Certificate, opts.Key)
90 if err != nil {
91 return nil, fmt.Errorf("tls client cert: %v", err)
92 }
93 cfg.Certificates = []tls.Certificate{cert}
94 } else if opts.LoadedCertificate != nil {
95 block := pem.Block{Type: "CERTIFICATE", Bytes: opts.LoadedCertificate.Raw}
96 certPem := pem.EncodeToMemory(&block)
97
98 var keyBytes []byte
99 switch k := opts.LoadedKey.(type) {
100 case *rsa.PrivateKey:
101 keyBytes = x509.MarshalPKCS1PrivateKey(k)
102 case *ecdsa.PrivateKey:
103 var err error
104 keyBytes, err = x509.MarshalECPrivateKey(k)
105 if err != nil {
106 return nil, fmt.Errorf("tls client priv key: %v", err)
107 }
108 default:
109 return nil, fmt.Errorf("tls client priv key: unsupported key type")
110 }
111
112 block = pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}
113 keyPem := pem.EncodeToMemory(&block)
114
115 cert, err := tls.X509KeyPair(certPem, keyPem)
116 if err != nil {
117 return nil, fmt.Errorf("tls client cert: %v", err)
118 }
119 cfg.Certificates = []tls.Certificate{cert}
120 }
121
122 cfg.InsecureSkipVerify = opts.InsecureSkipVerify
123
124 // When no CA certificate is provided, default to the system cert pool
125 // that way when a request is made to a server known by the system trust store,
126 // the name is still verified
127 if opts.LoadedCA != nil {
128 caCertPool := x509.NewCertPool()
129 caCertPool.AddCert(opts.LoadedCA)
130 cfg.RootCAs = caCertPool
131 } else if opts.CA != "" {
132 // load ca cert
133 caCert, err := ioutil.ReadFile(opts.CA)
134 if err != nil {
135 return nil, fmt.Errorf("tls client ca: %v", err)
136 }
137 caCertPool := x509.NewCertPool()
138 caCertPool.AppendCertsFromPEM(caCert)
139 cfg.RootCAs = caCertPool
140 }
141
142 // apply servername overrride
143 if opts.ServerName != "" {
144 cfg.InsecureSkipVerify = false
145 cfg.ServerName = opts.ServerName
146 }
147
148 cfg.BuildNameToCertificate()
149
150 return cfg, nil
151}
152
153// TLSTransport creates a http client transport suitable for mutual tls auth
154func TLSTransport(opts TLSClientOptions) (http.RoundTripper, error) {
155 cfg, err := TLSClientAuth(opts)
156 if err != nil {
157 return nil, err
158 }
159
160 return &http.Transport{TLSClientConfig: cfg}, nil
161}
162
163// TLSClient creates a http.Client for mutual auth
164func TLSClient(opts TLSClientOptions) (*http.Client, error) {
165 transport, err := TLSTransport(opts)
166 if err != nil {
167 return nil, err
168 }
169 return &http.Client{Transport: transport}, nil
170}
171
172// DefaultTimeout the default request timeout
173var DefaultTimeout = 30 * time.Second
174
175// Runtime represents an API client that uses the transport
176// to make http requests based on a swagger specification.
177type Runtime struct {
178 DefaultMediaType string
179 DefaultAuthentication runtime.ClientAuthInfoWriter
180 Consumers map[string]runtime.Consumer
181 Producers map[string]runtime.Producer
182
183 Transport http.RoundTripper
184 Jar http.CookieJar
185 //Spec *spec.Document
186 Host string
187 BasePath string
188 Formats strfmt.Registry
189 Context context.Context
190
191 Debug bool
192 logger logger.Logger
193
194 clientOnce *sync.Once
195 client *http.Client
196 schemes []string
197}
198
199// New creates a new default runtime for a swagger api runtime.Client
200func New(host, basePath string, schemes []string) *Runtime {
201 var rt Runtime
202 rt.DefaultMediaType = runtime.JSONMime
203
204 // TODO: actually infer this stuff from the spec
205 rt.Consumers = map[string]runtime.Consumer{
206 runtime.JSONMime: runtime.JSONConsumer(),
207 runtime.XMLMime: runtime.XMLConsumer(),
208 runtime.TextMime: runtime.TextConsumer(),
209 runtime.HTMLMime: runtime.TextConsumer(),
210 runtime.DefaultMime: runtime.ByteStreamConsumer(),
211 }
212 rt.Producers = map[string]runtime.Producer{
213 runtime.JSONMime: runtime.JSONProducer(),
214 runtime.XMLMime: runtime.XMLProducer(),
215 runtime.TextMime: runtime.TextProducer(),
216 runtime.HTMLMime: runtime.TextProducer(),
217 runtime.DefaultMime: runtime.ByteStreamProducer(),
218 }
219 rt.Transport = http.DefaultTransport
220 rt.Jar = nil
221 rt.Host = host
222 rt.BasePath = basePath
223 rt.Context = context.Background()
224 rt.clientOnce = new(sync.Once)
225 if !strings.HasPrefix(rt.BasePath, "/") {
226 rt.BasePath = "/" + rt.BasePath
227 }
228
229 rt.Debug = logger.DebugEnabled()
230 rt.logger = logger.StandardLogger{}
231
232 if len(schemes) > 0 {
233 rt.schemes = schemes
234 }
235 return &rt
236}
237
238// NewWithClient allows you to create a new transport with a configured http.Client
239func NewWithClient(host, basePath string, schemes []string, client *http.Client) *Runtime {
240 rt := New(host, basePath, schemes)
241 if client != nil {
242 rt.clientOnce.Do(func() {
243 rt.client = client
244 })
245 }
246 return rt
247}
248
249func (r *Runtime) pickScheme(schemes []string) string {
250 if v := r.selectScheme(r.schemes); v != "" {
251 return v
252 }
253 if v := r.selectScheme(schemes); v != "" {
254 return v
255 }
256 return "http"
257}
258
259func (r *Runtime) selectScheme(schemes []string) string {
260 schLen := len(schemes)
261 if schLen == 0 {
262 return ""
263 }
264
265 scheme := schemes[0]
266 // prefer https, but skip when not possible
267 if scheme != "https" && schLen > 1 {
268 for _, sch := range schemes {
269 if sch == "https" {
270 scheme = sch
271 break
272 }
273 }
274 }
275 return scheme
276}
277func transportOrDefault(left, right http.RoundTripper) http.RoundTripper {
278 if left == nil {
279 return right
280 }
281 return left
282}
283
284// EnableConnectionReuse drains the remaining body from a response
285// so that go will reuse the TCP connections.
286//
287// This is not enabled by default because there are servers where
288// the response never gets closed and that would make the code hang forever.
289// So instead it's provided as a http client middleware that can be used to override
290// any request.
291func (r *Runtime) EnableConnectionReuse() {
292 if r.client == nil {
293 r.Transport = KeepAliveTransport(
294 transportOrDefault(r.Transport, http.DefaultTransport),
295 )
296 return
297 }
298
299 r.client.Transport = KeepAliveTransport(
300 transportOrDefault(r.client.Transport,
301 transportOrDefault(r.Transport, http.DefaultTransport),
302 ),
303 )
304}
305
306// Submit a request and when there is a body on success it will turn that into the result
307// all other things are turned into an api error for swagger which retains the status code
308func (r *Runtime) Submit(operation *runtime.ClientOperation) (interface{}, error) {
309 params, readResponse, auth := operation.Params, operation.Reader, operation.AuthInfo
310
311 request, err := newRequest(operation.Method, operation.PathPattern, params)
312 if err != nil {
313 return nil, err
314 }
315
316 var accept []string
317 accept = append(accept, operation.ProducesMediaTypes...)
318 if err = request.SetHeaderParam(runtime.HeaderAccept, accept...); err != nil {
319 return nil, err
320 }
321
322 if auth == nil && r.DefaultAuthentication != nil {
323 auth = r.DefaultAuthentication
324 }
325 //if auth != nil {
326 // if err := auth.AuthenticateRequest(request, r.Formats); err != nil {
327 // return nil, err
328 // }
329 //}
330
331 // TODO: pick appropriate media type
332 cmt := r.DefaultMediaType
333 for _, mediaType := range operation.ConsumesMediaTypes {
334 // Pick first non-empty media type
335 if mediaType != "" {
336 cmt = mediaType
337 break
338 }
339 }
340
341 if _, ok := r.Producers[cmt]; !ok && cmt != runtime.MultipartFormMime && cmt != runtime.URLencodedFormMime {
342 return nil, fmt.Errorf("none of producers: %v registered. try %s", r.Producers, cmt)
343 }
344
345 req, err := request.buildHTTP(cmt, r.BasePath, r.Producers, r.Formats, auth)
346 if err != nil {
347 return nil, err
348 }
349 req.URL.Scheme = r.pickScheme(operation.Schemes)
350 req.URL.Host = r.Host
351
352 r.clientOnce.Do(func() {
353 r.client = &http.Client{
354 Transport: r.Transport,
355 Jar: r.Jar,
356 }
357 })
358
359 if r.Debug {
360 b, err2 := httputil.DumpRequestOut(req, true)
361 if err2 != nil {
362 return nil, err2
363 }
364 r.logger.Debugf("%s\n", string(b))
365 }
366
367 var hasTimeout bool
368 pctx := operation.Context
369 if pctx == nil {
370 pctx = r.Context
371 } else {
372 hasTimeout = true
373 }
374 if pctx == nil {
375 pctx = context.Background()
376 }
377 var ctx context.Context
378 var cancel context.CancelFunc
379 if hasTimeout {
380 ctx, cancel = context.WithCancel(pctx)
381 } else {
382 ctx, cancel = context.WithTimeout(pctx, request.timeout)
383 }
384 defer cancel()
385
386 client := operation.Client
387 if client == nil {
388 client = r.client
389 }
390 req = req.WithContext(ctx)
391 res, err := client.Do(req) // make requests, by default follows 10 redirects before failing
392 if err != nil {
393 return nil, err
394 }
395 defer res.Body.Close()
396
397 if r.Debug {
398 b, err2 := httputil.DumpResponse(res, true)
399 if err2 != nil {
400 return nil, err2
401 }
402 r.logger.Debugf("%s\n", string(b))
403 }
404
405 ct := res.Header.Get(runtime.HeaderContentType)
406 if ct == "" { // this should really really never occur
407 ct = r.DefaultMediaType
408 }
409
410 mt, _, err := mime.ParseMediaType(ct)
411 if err != nil {
412 return nil, fmt.Errorf("parse content type: %s", err)
413 }
414
415 cons, ok := r.Consumers[mt]
416 if !ok {
417 if cons, ok = r.Consumers["*/*"]; !ok {
418 // scream about not knowing what to do
419 return nil, fmt.Errorf("no consumer: %q", ct)
420 }
421 }
422 return readResponse.ReadResponse(response{res}, cons)
423}
424
425// SetDebug changes the debug flag.
426// It ensures that client and middlewares have the set debug level.
427func (r *Runtime) SetDebug(debug bool) {
428 r.Debug = debug
429 middleware.Debug = debug
430}
431
432// SetLogger changes the logger stream.
433// It ensures that client and middlewares use the same logger.
434func (r *Runtime) SetLogger(logger logger.Logger) {
435 r.logger = logger
436 middleware.Logger = logger
437}