1package rc
2
3import (
4	"crypto/tls"
5	"crypto/x509"
6	"errors"
7	"fmt"
8	"net"
9	"net/http"
10	"os"
11	"runtime"
12	"time"
13
14	conc "github.com/concourse/concourse"
15	"github.com/concourse/concourse/atc"
16	"github.com/concourse/concourse/fly/ui"
17	"github.com/concourse/concourse/fly/version"
18	"github.com/concourse/concourse/go-concourse/concourse"
19	semisemanticversion "github.com/cppforlife/go-semi-semantic/version"
20	"golang.org/x/oauth2"
21)
22
23type ErrVersionMismatch struct {
24	flyVersion string
25	atcVersion string
26	targetName TargetName
27}
28
29func NewErrVersionMismatch(flyVersion string, atcVersion string, targetName TargetName) ErrVersionMismatch {
30	return ErrVersionMismatch{
31		flyVersion: flyVersion,
32		atcVersion: atcVersion,
33		targetName: targetName,
34	}
35}
36
37func (e ErrVersionMismatch) Error() string {
38	return fmt.Sprintf(
39		"fly version (%s) is out of sync with the target (%s). to sync up, run the following:\n\n    %s -t %s sync\n",
40		ui.Embolden(e.flyVersion), ui.Embolden(e.atcVersion), os.Args[0], e.targetName)
41}
42
43//go:generate counterfeiter . Target
44
45type Target interface {
46	Client() concourse.Client
47	Team() concourse.Team
48	FindTeam(string) (concourse.Team, error)
49	CACert() string
50	ClientCertPath() string
51	ClientKeyPath() string
52	ClientCertificate() []tls.Certificate
53	Validate() error
54	ValidateWithWarningOnly() error
55	TLSConfig() *tls.Config
56	URL() string
57	WorkerVersion() (string, error)
58	IsWorkerVersionCompatible(string) (bool, error)
59	Token() *TargetToken
60	TokenAuthorization() (string, bool)
61	Version() (string, error)
62}
63
64type target struct {
65	name              TargetName
66	teamName          string
67	caCert            string
68	clientCertPath    string
69	clientKeyPath     string
70	clientCertificate []tls.Certificate
71	tlsConfig         *tls.Config
72	client            concourse.Client
73	url               string
74	token             *TargetToken
75	info              atc.Info
76}
77
78func NewTarget(
79	name TargetName,
80	teamName string,
81	url string,
82	token *TargetToken,
83	caCert string,
84	caCertPool *x509.CertPool,
85	clientCertPath string,
86	clientKeyPath string,
87	clientCertificate []tls.Certificate,
88	insecure bool,
89	client concourse.Client,
90) *target {
91	tlsConfig := &tls.Config{
92		InsecureSkipVerify: insecure,
93		RootCAs:            caCertPool,
94		Certificates:       clientCertificate,
95	}
96
97	return &target{
98		name:              name,
99		teamName:          teamName,
100		url:               url,
101		token:             token,
102		caCert:            caCert,
103		clientCertPath:    clientCertPath,
104		clientKeyPath:     clientKeyPath,
105		clientCertificate: clientCertificate,
106		tlsConfig:         tlsConfig,
107		client:            client,
108	}
109}
110
111func LoadTargetFromURL(url, team string, tracing bool) (Target, TargetName, error) {
112	flyTargets, err := LoadTargets()
113	if err != nil {
114		return nil, "", err
115	}
116
117	for name, props := range flyTargets {
118		if props.API == url && props.TeamName == team {
119			target, err := LoadTarget(name, tracing)
120			return target, name, err
121		}
122	}
123
124	return nil, "", ErrNoTargetFromURL
125}
126
127func LoadTarget(selectedTarget TargetName, tracing bool) (Target, error) {
128	var clientCertificate []tls.Certificate
129
130	targetProps, err := selectTarget(selectedTarget)
131	if err != nil {
132		return nil, err
133	}
134
135	caCertPool, err := loadCACertPool(targetProps.CACert)
136	if err != nil {
137		return nil, err
138	}
139
140	clientCertificate, err = loadClientCertificate(targetProps.ClientCertPath, targetProps.ClientKeyPath)
141	if err != nil {
142		return nil, err
143	}
144
145	httpClient := defaultHttpClient(targetProps.Token, targetProps.Insecure, caCertPool, clientCertificate)
146	client := concourse.NewClient(targetProps.API, httpClient, tracing)
147
148	return NewTarget(
149		selectedTarget,
150		targetProps.TeamName,
151		targetProps.API,
152		targetProps.Token,
153		targetProps.CACert,
154		caCertPool,
155		targetProps.ClientCertPath,
156		targetProps.ClientKeyPath,
157		clientCertificate,
158		targetProps.Insecure,
159		client,
160	), nil
161}
162
163func LoadUnauthenticatedTarget(
164	selectedTarget TargetName,
165	teamName string,
166	insecure bool,
167	caCert string,
168	clientCertPath string,
169	clientKeyPath string,
170	tracing bool,
171) (Target, error) {
172	targetProps, err := selectTarget(selectedTarget)
173	if err != nil {
174		return nil, err
175	}
176
177	if teamName == "" {
178		teamName = targetProps.TeamName
179	}
180
181	if caCert == "" {
182		caCert = targetProps.CACert
183	}
184
185	if insecure {
186		caCert = ""
187	}
188
189	caCertPool, err := loadCACertPool(caCert)
190	if err != nil {
191		return nil, err
192	}
193
194	var clientCertificate []tls.Certificate
195
196	if clientCertPath == "" && clientKeyPath == "" {
197		clientCertPath = targetProps.ClientCertPath
198		clientKeyPath = targetProps.ClientKeyPath
199	}
200
201	clientCertificate, err = loadClientCertificate(clientCertPath, clientKeyPath)
202	if err != nil {
203		return nil, err
204	}
205
206	httpClient := &http.Client{Transport: transport(insecure, caCertPool, clientCertificate)}
207
208	return NewTarget(
209		selectedTarget,
210		teamName,
211		targetProps.API,
212		targetProps.Token,
213		caCert,
214		caCertPool,
215		clientCertPath,
216		clientKeyPath,
217		clientCertificate,
218		targetProps.Insecure,
219		concourse.NewClient(targetProps.API, httpClient, tracing),
220	), nil
221}
222
223func NewUnauthenticatedTarget(
224	name TargetName,
225	url string,
226	teamName string,
227	insecure bool,
228	caCert string,
229	clientCertPath string,
230	clientKeyPath string,
231	tracing bool,
232) (Target, error) {
233	caCertPool, err := loadCACertPool(caCert)
234	if err != nil {
235		return nil, err
236	}
237
238	var clientCertificate []tls.Certificate
239	clientCertificate, err = loadClientCertificate(clientCertPath, clientKeyPath)
240	if err != nil {
241		return nil, err
242	}
243
244	httpClient := &http.Client{Transport: transport(insecure, caCertPool, clientCertificate)}
245	client := concourse.NewClient(url, httpClient, tracing)
246	return NewTarget(
247		name,
248		teamName,
249		url,
250		nil,
251		caCert,
252		caCertPool,
253		clientCertPath,
254		clientKeyPath,
255		clientCertificate,
256		insecure,
257		client,
258	), nil
259}
260
261func NewAuthenticatedTarget(
262	name TargetName,
263	url string,
264	teamName string,
265	insecure bool,
266	token *TargetToken,
267	caCert string,
268	clientCertPath string,
269	clientKeyPath string,
270	tracing bool,
271) (Target, error) {
272	caCertPool, err := loadCACertPool(caCert)
273	if err != nil {
274		return nil, err
275	}
276
277	var clientCertificate []tls.Certificate
278	clientCertificate, err = loadClientCertificate(clientCertPath, clientKeyPath)
279	if err != nil {
280		return nil, err
281	}
282
283	httpClient := defaultHttpClient(token, insecure, caCertPool, clientCertificate)
284	client := concourse.NewClient(url, httpClient, tracing)
285
286	return NewTarget(
287		name,
288		teamName,
289		url,
290		token,
291		caCert,
292		caCertPool,
293		clientCertPath,
294		clientKeyPath,
295		clientCertificate,
296		insecure,
297		client,
298	), nil
299}
300
301func NewBasicAuthTarget(
302	name TargetName,
303	url string,
304	teamName string,
305	insecure bool,
306	username string,
307	password string,
308	caCert string,
309	clientCertPath string,
310	clientKeyPath string,
311	tracing bool,
312) (Target, error) {
313	caCertPool, err := loadCACertPool(caCert)
314	if err != nil {
315		return nil, err
316	}
317
318	var clientCertificate []tls.Certificate
319	clientCertificate, err = loadClientCertificate(clientCertPath, clientKeyPath)
320	if err != nil {
321		return nil, err
322	}
323
324	httpClient := basicAuthHttpClient(username, password, insecure, caCertPool, clientCertificate)
325	client := concourse.NewClient(url, httpClient, tracing)
326
327	return NewTarget(
328		name,
329		teamName,
330		url,
331		nil,
332		caCert,
333		caCertPool,
334		clientCertPath,
335		clientKeyPath,
336		clientCertificate,
337		insecure,
338		client,
339	), nil
340}
341
342func (t *target) Client() concourse.Client {
343	return t.client
344}
345
346func (t *target) Team() concourse.Team {
347	return t.client.Team(t.teamName)
348}
349
350func (t *target) FindTeam(teamName string) (concourse.Team, error) {
351	return t.client.FindTeam(teamName)
352}
353
354func (t *target) CACert() string {
355	return t.caCert
356}
357
358func (t *target) TLSConfig() *tls.Config {
359	return t.tlsConfig
360}
361
362func (t *target) ClientCertPath() string {
363	return t.clientCertPath
364}
365
366func (t *target) ClientKeyPath() string {
367	return t.clientKeyPath
368}
369
370func (t *target) ClientCertificate() []tls.Certificate {
371	return t.clientCertificate
372}
373
374func (t *target) URL() string {
375	return t.url
376}
377
378func (t *target) Token() *TargetToken {
379	return t.token
380}
381
382func (t *target) Version() (string, error) {
383	info, err := t.getInfo()
384	if err != nil {
385		return "", err
386	}
387
388	return info.Version, nil
389}
390
391func (t *target) WorkerVersion() (string, error) {
392	info, err := t.getInfo()
393	if err != nil {
394		return "", err
395	}
396
397	return info.WorkerVersion, nil
398}
399
400func (t *target) TokenAuthorization() (string, bool) {
401	if t.token == nil || (t.token.Type == "" && t.token.Value == "") {
402		return "", false
403	}
404
405	return t.token.Type + " " + t.token.Value, true
406}
407
408func (t *target) ValidateWithWarningOnly() error {
409	return t.validate(true)
410}
411
412func (t *target) Validate() error {
413	return t.validate(false)
414}
415
416func (t *target) IsWorkerVersionCompatible(workerVersion string) (bool, error) {
417	info, err := t.getInfo()
418	if err != nil {
419		return false, err
420	}
421
422	if info.WorkerVersion == "" {
423		return true, nil
424	}
425
426	if workerVersion == "" {
427		return false, nil
428	}
429
430	workerV, err := semisemanticversion.NewVersionFromString(workerVersion)
431	if err != nil {
432		return false, err
433	}
434
435	infoV, err := semisemanticversion.NewVersionFromString(info.WorkerVersion)
436	if err != nil {
437		return false, err
438	}
439
440	if workerV.Release.Components[0].Compare(infoV.Release.Components[0]) != 0 {
441		return false, nil
442	}
443
444	if workerV.Release.Components[1].Compare(infoV.Release.Components[1]) == -1 {
445		return false, nil
446	}
447
448	return true, nil
449}
450
451func (t *target) validate(allowVersionMismatch bool) error {
452	info, err := t.getInfo()
453	if err != nil {
454		return err
455	}
456
457	if info.Version == conc.Version || version.IsDev(conc.Version) {
458		return nil
459	}
460
461	atcMajor, atcMinor, atcPatch, err := version.GetSemver(info.Version)
462	if err != nil {
463		return err
464	}
465
466	flyMajor, flyMinor, flyPatch, err := version.GetSemver(conc.Version)
467	if err != nil {
468		return err
469	}
470
471	if !allowVersionMismatch && (atcMajor != flyMajor || atcMinor != flyMinor) {
472		return NewErrVersionMismatch(conc.Version, info.Version, t.name)
473	}
474
475	if atcMajor != flyMajor || atcMinor != flyMinor || atcPatch != flyPatch {
476		fmt.Fprintln(ui.Stderr, ui.WarningColor("WARNING:\n"))
477		fmt.Fprintln(ui.Stderr, ui.WarningColor(NewErrVersionMismatch(conc.Version, info.Version, t.name).Error()))
478	}
479
480	return nil
481}
482
483func (t *target) getInfo() (atc.Info, error) {
484	if (t.info != atc.Info{}) {
485		return t.info, nil
486	}
487
488	var err error
489	t.info, err = t.client.GetInfo()
490	return t.info, err
491}
492
493func defaultHttpClient(token *TargetToken, insecure bool, caCertPool *x509.CertPool, clientCertificate []tls.Certificate) *http.Client {
494	var oAuthToken *oauth2.Token
495	if token != nil {
496		oAuthToken = &oauth2.Token{
497			TokenType:   token.Type,
498			AccessToken: token.Value,
499		}
500	}
501
502	transport := transport(insecure, caCertPool, clientCertificate)
503
504	if token != nil {
505		transport = &oauth2.Transport{
506			Source: oauth2.StaticTokenSource(oAuthToken),
507			Base:   transport,
508		}
509	}
510
511	return &http.Client{Transport: transport}
512}
513
514func loadCACertPool(caCert string) (cert *x509.CertPool, err error) {
515	if caCert == "" {
516		return nil, nil
517	}
518
519	// TODO: remove else block once we switch to go 1.8
520	// x509.SystemCertPool is not supported in go 1.7 on Windows
521	// see: https://github.com/golang/go/issues/16736
522	var pool *x509.CertPool
523	if runtime.GOOS != "windows" {
524		var err error
525		pool, err = x509.SystemCertPool()
526		if err != nil {
527			return nil, err
528		}
529	} else {
530		pool = x509.NewCertPool()
531	}
532
533	ok := pool.AppendCertsFromPEM([]byte(caCert))
534	if !ok {
535		return nil, errors.New("CA Cert not valid")
536	}
537	return pool, nil
538}
539
540func loadClientCertificate(clientCertificateLocation string, clientKeyLocation string) (cert []tls.Certificate, err error) {
541	if clientCertificateLocation == "" {
542		if clientKeyLocation != "" {
543			err = errors.New("A client key may not be declared without defining a client certificate")
544
545			return []tls.Certificate{}, err
546		}
547
548		return []tls.Certificate{}, nil
549	}
550
551	if clientCertificateLocation != "" && clientKeyLocation == "" {
552		err = errors.New("A client certificate may not be declared without defining a client key")
553
554		return []tls.Certificate{}, err
555	}
556
557	clientCertData, err := tls.LoadX509KeyPair(clientCertificateLocation, clientKeyLocation)
558	if err != nil {
559		return []tls.Certificate{}, err
560	}
561
562	cert = []tls.Certificate{clientCertData}
563
564	return cert, nil
565}
566
567func basicAuthHttpClient(
568	username string,
569	password string,
570	insecure bool,
571	caCertPool *x509.CertPool,
572	clientCertificate []tls.Certificate,
573) *http.Client {
574	return &http.Client{
575		Transport: basicAuthTransport{
576			username: username,
577			password: password,
578			base:     transport(insecure, caCertPool, clientCertificate),
579		},
580	}
581}
582
583func transport(insecure bool, caCertPool *x509.CertPool, clientCertificate []tls.Certificate) http.RoundTripper {
584	var transport http.RoundTripper
585
586	transport = &http.Transport{
587		TLSClientConfig: &tls.Config{
588			InsecureSkipVerify: insecure,
589			RootCAs:            caCertPool,
590			Certificates:       clientCertificate,
591		},
592		Dial: (&net.Dialer{
593			Timeout: 10 * time.Second,
594		}).Dial,
595		Proxy: http.ProxyFromEnvironment,
596	}
597
598	return transport
599}
600
601type basicAuthTransport struct {
602	username string
603	password string
604
605	base http.RoundTripper
606}
607
608func (t basicAuthTransport) RoundTrip(r *http.Request) (*http.Response, error) {
609	r.SetBasicAuth(t.username, t.password)
610	return t.base.RoundTrip(r)
611}
612