1// 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	"crypto/tls"
19	"crypto/x509"
20	"fmt"
21	"io/ioutil"
22	"mime"
23	"net/http"
24	"net/http/httputil"
25	"os"
26	"path"
27	"strings"
28	"sync"
29	"time"
30
31	"golang.org/x/net/context"
32	"golang.org/x/net/context/ctxhttp"
33
34	"github.com/go-openapi/runtime"
35	"github.com/go-openapi/strfmt"
36)
37
38// TLSClientOptions to configure client authentication with mutual TLS
39type TLSClientOptions struct {
40	Certificate        string
41	Key                string
42	CA                 string
43	ServerName         string
44	InsecureSkipVerify bool
45	_                  struct{}
46}
47
48// TLSClientAuth creates a tls.Config for mutual auth
49func TLSClientAuth(opts TLSClientOptions) (*tls.Config, error) {
50	// load client cert
51	cert, err := tls.LoadX509KeyPair(opts.Certificate, opts.Key)
52	if err != nil {
53		return nil, fmt.Errorf("tls client cert: %v", err)
54	}
55
56	// create client tls config
57	cfg := &tls.Config{}
58	cfg.Certificates = []tls.Certificate{cert}
59	cfg.InsecureSkipVerify = opts.InsecureSkipVerify
60
61	// When no CA certificate is provided, default to the system cert pool
62	// that way when a request is made to a server known by the system trust store,
63	// the name is still verified
64	if opts.CA != "" {
65		// load ca cert
66		caCert, err := ioutil.ReadFile(opts.CA)
67		if err != nil {
68			return nil, fmt.Errorf("tls client ca: %v", err)
69		}
70		caCertPool := x509.NewCertPool()
71		caCertPool.AppendCertsFromPEM(caCert)
72		cfg.RootCAs = caCertPool
73	}
74
75	// apply servername overrride
76	if opts.ServerName != "" {
77		cfg.InsecureSkipVerify = false
78		cfg.ServerName = opts.ServerName
79	}
80
81	cfg.BuildNameToCertificate()
82
83	return cfg, nil
84}
85
86// TLSTransport creates a http client transport suitable for mutual tls auth
87func TLSTransport(opts TLSClientOptions) (http.RoundTripper, error) {
88	cfg, err := TLSClientAuth(opts)
89	if err != nil {
90		return nil, err
91	}
92
93	return &http.Transport{TLSClientConfig: cfg}, nil
94}
95
96// TLSClient creates a http.Client for mutual auth
97func TLSClient(opts TLSClientOptions) (*http.Client, error) {
98	transport, err := TLSTransport(opts)
99	if err != nil {
100		return nil, err
101	}
102	return &http.Client{Transport: transport}, nil
103}
104
105// DefaultTimeout the default request timeout
106var DefaultTimeout = 30 * time.Second
107
108// Runtime represents an API client that uses the transport
109// to make http requests based on a swagger specification.
110type Runtime struct {
111	DefaultMediaType      string
112	DefaultAuthentication runtime.ClientAuthInfoWriter
113	Consumers             map[string]runtime.Consumer
114	Producers             map[string]runtime.Producer
115
116	Transport http.RoundTripper
117	Jar       http.CookieJar
118	//Spec      *spec.Document
119	Host     string
120	BasePath string
121	Formats  strfmt.Registry
122	Debug    bool
123	Context  context.Context
124
125	clientOnce *sync.Once
126	client     *http.Client
127	schemes    []string
128	do         func(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error)
129}
130
131// New creates a new default runtime for a swagger api runtime.Client
132func New(host, basePath string, schemes []string) *Runtime {
133	var rt Runtime
134	rt.DefaultMediaType = runtime.JSONMime
135
136	// TODO: actually infer this stuff from the spec
137	rt.Consumers = map[string]runtime.Consumer{
138		runtime.JSONMime:    runtime.JSONConsumer(),
139		runtime.XMLMime:     runtime.XMLConsumer(),
140		runtime.TextMime:    runtime.TextConsumer(),
141		runtime.DefaultMime: runtime.ByteStreamConsumer(),
142	}
143	rt.Producers = map[string]runtime.Producer{
144		runtime.JSONMime:    runtime.JSONProducer(),
145		runtime.XMLMime:     runtime.XMLProducer(),
146		runtime.TextMime:    runtime.TextProducer(),
147		runtime.DefaultMime: runtime.ByteStreamProducer(),
148	}
149	rt.Transport = http.DefaultTransport
150	rt.Jar = nil
151	rt.Host = host
152	rt.BasePath = basePath
153	rt.Context = context.Background()
154	rt.clientOnce = new(sync.Once)
155	if !strings.HasPrefix(rt.BasePath, "/") {
156		rt.BasePath = "/" + rt.BasePath
157	}
158	rt.Debug = len(os.Getenv("DEBUG")) > 0
159	if len(schemes) > 0 {
160		rt.schemes = schemes
161	}
162	rt.do = ctxhttp.Do
163	return &rt
164}
165
166// NewWithClient allows you to create a new transport with a configured http.Client
167func NewWithClient(host, basePath string, schemes []string, client *http.Client) *Runtime {
168	rt := New(host, basePath, schemes)
169	if client != nil {
170		rt.clientOnce.Do(func() {
171			rt.client = client
172		})
173	}
174	return rt
175}
176
177func (r *Runtime) pickScheme(schemes []string) string {
178	if v := r.selectScheme(r.schemes); v != "" {
179		return v
180	}
181	if v := r.selectScheme(schemes); v != "" {
182		return v
183	}
184	return "http"
185}
186
187func (r *Runtime) selectScheme(schemes []string) string {
188	schLen := len(schemes)
189	if schLen == 0 {
190		return ""
191	}
192
193	scheme := schemes[0]
194	// prefer https, but skip when not possible
195	if scheme != "https" && schLen > 1 {
196		for _, sch := range schemes {
197			if sch == "https" {
198				scheme = sch
199				break
200			}
201		}
202	}
203	return scheme
204}
205
206// Submit a request and when there is a body on success it will turn that into the result
207// all other things are turned into an api error for swagger which retains the status code
208func (r *Runtime) Submit(operation *runtime.ClientOperation) (interface{}, error) {
209	params, readResponse, auth := operation.Params, operation.Reader, operation.AuthInfo
210
211	request, err := newRequest(operation.Method, operation.PathPattern, params)
212	if err != nil {
213		return nil, err
214	}
215
216	var accept []string
217	for _, mimeType := range operation.ProducesMediaTypes {
218		accept = append(accept, mimeType)
219	}
220	request.SetHeaderParam(runtime.HeaderAccept, accept...)
221
222	if auth == nil && r.DefaultAuthentication != nil {
223		auth = r.DefaultAuthentication
224	}
225	if auth != nil {
226		if err := auth.AuthenticateRequest(request, r.Formats); err != nil {
227			return nil, err
228		}
229	}
230
231	// TODO: pick appropriate media type
232	cmt := r.DefaultMediaType
233	if len(operation.ConsumesMediaTypes) > 0 {
234		cmt = operation.ConsumesMediaTypes[0]
235	}
236
237	req, err := request.BuildHTTP(cmt, r.Producers, r.Formats)
238	if err != nil {
239		return nil, err
240	}
241	req.URL.Scheme = r.pickScheme(operation.Schemes)
242	req.URL.Host = r.Host
243	var reinstateSlash bool
244	if req.URL.Path != "" && req.URL.Path != "/" && req.URL.Path[len(req.URL.Path)-1] == '/' {
245		reinstateSlash = true
246	}
247	req.URL.Path = path.Join(r.BasePath, req.URL.Path)
248	if reinstateSlash {
249		req.URL.Path = req.URL.Path + "/"
250	}
251
252	r.clientOnce.Do(func() {
253		r.client = &http.Client{
254			Transport: r.Transport,
255			Jar:       r.Jar,
256		}
257	})
258
259	if r.Debug {
260		b, err2 := httputil.DumpRequestOut(req, true)
261		if err2 != nil {
262			return nil, err2
263		}
264		fmt.Fprintln(os.Stderr, string(b))
265	}
266
267	var hasTimeout bool
268	pctx := operation.Context
269	if pctx == nil {
270		pctx = r.Context
271	} else {
272		hasTimeout = true
273	}
274	if pctx == nil {
275		pctx = context.Background()
276	}
277	var ctx context.Context
278	var cancel context.CancelFunc
279	if hasTimeout {
280		ctx, cancel = context.WithCancel(pctx)
281	} else {
282		ctx, cancel = context.WithTimeout(pctx, request.timeout)
283	}
284	defer cancel()
285
286	client := operation.Client
287	if client == nil {
288		client = r.client
289	}
290	if r.do == nil {
291		r.do = ctxhttp.Do
292	}
293	res, err := r.do(ctx, client, req) // make requests, by default follows 10 redirects before failing
294	if err != nil {
295		return nil, err
296	}
297	defer res.Body.Close()
298
299	if r.Debug {
300		b, err2 := httputil.DumpResponse(res, true)
301		if err2 != nil {
302			return nil, err2
303		}
304		fmt.Fprintln(os.Stderr, string(b))
305	}
306
307	ct := res.Header.Get(runtime.HeaderContentType)
308	if ct == "" { // this should really really never occur
309		ct = r.DefaultMediaType
310	}
311
312	mt, _, err := mime.ParseMediaType(ct)
313	if err != nil {
314		return nil, fmt.Errorf("parse content type: %s", err)
315	}
316
317	cons, ok := r.Consumers[mt]
318	if !ok {
319		// scream about not knowing what to do
320		return nil, fmt.Errorf("no consumer: %q", ct)
321	}
322	return readResponse.ReadResponse(response{res}, cons)
323}
324