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