Browse code

o Add basic validation for route TLS configuration - checks that input is "syntactically" valid. o Checkpoint initial code. o Add support for validating route tls config. o Add option for validating route tls config. o Validation fixes. o Check private key + cert mismatches. o Add tests. o Record route rejection. o Hook into add route processing + store invalid service alias configs in another place - easy to check prior errors on readmission. o Remove entry from invalid service alias configs upon route removal. o Add generated completions. o Bug fixes. o Recording rejecting routes is not working completely. o Fix status update problem - we should set the status to admitted only if we had no errors handling a route. o Rework to use a new controller - extended_validator as per @smarterclayton comments. o Cleanup validation as per @liggitt comments. o Update bash completions. o Fixup older validation unit tests. o Changes as per @liggitt review comments + cleanup tests. o Fix failing test.

ramr authored on 2016/04/06 09:36:20
Showing 9 changed files
... ...
@@ -15651,6 +15651,7 @@ _openshift_infra_router()
15651 15651
     flags+=("--context=")
15652 15652
     flags+=("--default-certificate=")
15653 15653
     flags+=("--default-certificate-path=")
15654
+    flags+=("--extended-validation")
15654 15655
     flags+=("--fields=")
15655 15656
     flags+=("--hostname-template=")
15656 15657
     flags+=("--include-udp-endpoints")
... ...
@@ -15,6 +15,7 @@ import (
15 15
 
16 16
 	"github.com/openshift/origin/pkg/cmd/util"
17 17
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
18
+	"github.com/openshift/origin/pkg/router"
18 19
 	"github.com/openshift/origin/pkg/router/controller"
19 20
 	templateplugin "github.com/openshift/origin/pkg/router/template"
20 21
 	"github.com/openshift/origin/pkg/util/proc"
... ...
@@ -54,6 +55,7 @@ type TemplateRouter struct {
54 54
 	ReloadInterval         time.Duration
55 55
 	DefaultCertificate     string
56 56
 	DefaultCertificatePath string
57
+	ExtendedValidation     bool
57 58
 	RouterService          *ktypes.NamespacedName
58 59
 }
59 60
 
... ...
@@ -77,6 +79,7 @@ func (o *TemplateRouter) Bind(flag *pflag.FlagSet) {
77 77
 	flag.StringVar(&o.TemplateFile, "template", util.Env("TEMPLATE_FILE", ""), "The path to the template file to use")
78 78
 	flag.StringVar(&o.ReloadScript, "reload", util.Env("RELOAD_SCRIPT", ""), "The path to the reload script to use")
79 79
 	flag.DurationVar(&o.ReloadInterval, "interval", reloadInterval(), "Controls how often router reloads are invoked. Mutiple router reload requests are coalesced for the duration of this interval since the last reload time.")
80
+	flag.BoolVar(&o.ExtendedValidation, "extended-validation", util.Env("EXTENDED_VALIDATION", "") == "true", "If set, then an additional extended validation step is performed on all routes admitted in by this router.")
80 81
 }
81 82
 
82 83
 type RouterStats struct {
... ...
@@ -192,7 +195,11 @@ func (o *TemplateRouterOptions) Run() error {
192 192
 	}
193 193
 
194 194
 	statusPlugin := controller.NewStatusAdmitter(templatePlugin, oc, o.RouterName)
195
-	plugin := controller.NewUniqueHost(statusPlugin, o.RouteSelectionFunc(), statusPlugin)
195
+	var nextPlugin router.Plugin = statusPlugin
196
+	if o.ExtendedValidation {
197
+		nextPlugin = controller.NewExtendedValidator(nextPlugin, controller.RejectionRecorder(statusPlugin))
198
+	}
199
+	plugin := controller.NewUniqueHost(nextPlugin, o.RouteSelectionFunc(), controller.RejectionRecorder(statusPlugin))
196 200
 
197 201
 	factory := o.RouterSelection.NewFactory(oc, kc)
198 202
 	controller := factory.Create(plugin)
... ...
@@ -72,6 +72,8 @@ type RouteIngressConditionType string
72 72
 const (
73 73
 	// RouteAdmitted means the route is able to service requests for the provided Host
74 74
 	RouteAdmitted RouteIngressConditionType = "Admitted"
75
+	// RouteExtendedValidationFailed means the route configuration failed an extended validation check.
76
+	RouteExtendedValidationFailed RouteIngressConditionType = "ExtendedValidationFailed"
75 77
 	// TODO: add other route condition types
76 78
 )
77 79
 
... ...
@@ -1,6 +1,9 @@
1 1
 package validation
2 2
 
3 3
 import (
4
+	"crypto/tls"
5
+	"crypto/x509"
6
+	"encoding/pem"
4 7
 	"fmt"
5 8
 	"strings"
6 9
 
... ...
@@ -76,6 +79,67 @@ func ValidateRouteStatusUpdate(route *routeapi.Route, older *routeapi.Route) fie
76 76
 	return allErrs
77 77
 }
78 78
 
79
+// ExtendedValidateRoute performs an extended validation on the route
80
+// including checking that the TLS config is valid.
81
+func ExtendedValidateRoute(route *routeapi.Route) field.ErrorList {
82
+	tlsConfig := route.Spec.TLS
83
+	result := field.ErrorList{}
84
+
85
+	if tlsConfig == nil {
86
+		return result
87
+	}
88
+
89
+	tlsFieldPath := field.NewPath("spec").Child("tls")
90
+	if errs := validateTLS(route, tlsFieldPath); len(errs) != 0 {
91
+		result = append(result, errs...)
92
+	}
93
+
94
+	// TODO: Check if we can be stricter with validating the certificate
95
+	//       is for the route hostname. Don't want existing routes to
96
+	//       break, so disable the hostname validation for now.
97
+	// hostname := route.Spec.Host
98
+	hostname := ""
99
+	var certPool *x509.CertPool
100
+
101
+	if len(tlsConfig.CACertificate) > 0 {
102
+		certPool = x509.NewCertPool()
103
+		if ok := certPool.AppendCertsFromPEM([]byte(tlsConfig.CACertificate)); !ok {
104
+			result = append(result, field.Invalid(tlsFieldPath.Child("caCertificate"), tlsConfig.CACertificate, "failed to parse CA certificate"))
105
+		}
106
+	}
107
+
108
+	verifyOptions := &x509.VerifyOptions{
109
+		DNSName: hostname,
110
+		Roots:   certPool,
111
+	}
112
+
113
+	if len(tlsConfig.Certificate) > 0 {
114
+		if _, err := validateCertificatePEM(tlsConfig.Certificate, verifyOptions); err != nil {
115
+			result = append(result, field.Invalid(tlsFieldPath.Child("certificate"), tlsConfig.Certificate, err.Error()))
116
+		}
117
+
118
+		certKeyBytes := []byte{}
119
+		certKeyBytes = append(certKeyBytes, []byte(tlsConfig.Certificate)...)
120
+		if len(tlsConfig.Key) > 0 {
121
+			certKeyBytes = append(certKeyBytes, byte('\n'))
122
+			certKeyBytes = append(certKeyBytes, []byte(tlsConfig.Key)...)
123
+		}
124
+
125
+		if _, err := tls.X509KeyPair(certKeyBytes, certKeyBytes); err != nil {
126
+			result = append(result, field.Invalid(tlsFieldPath.Child("key"), tlsConfig.Key, err.Error()))
127
+		}
128
+	}
129
+
130
+	if len(tlsConfig.DestinationCACertificate) > 0 {
131
+		roots := x509.NewCertPool()
132
+		if ok := roots.AppendCertsFromPEM([]byte(tlsConfig.DestinationCACertificate)); !ok {
133
+			result = append(result, field.Invalid(tlsFieldPath.Child("destinationCACertificate"), tlsConfig.DestinationCACertificate, "failed to parse destination CA certificate"))
134
+		}
135
+	}
136
+
137
+	return result
138
+}
139
+
79 140
 // validateTLS tests fields for different types of TLS combinations are set.  Called
80 141
 // by ValidateRoute.
81 142
 func validateTLS(route *routeapi.Route, fldPath *field.Path) field.ErrorList {
... ...
@@ -158,3 +222,38 @@ func validateInsecureEdgeTerminationPolicy(tls *routeapi.TLSConfig, fldPath *fie
158 158
 
159 159
 	return nil
160 160
 }
161
+
162
+// validateCertificatePEM checks if a certificate PEM is valid and
163
+// optionally verifies the certificate using the options.
164
+func validateCertificatePEM(certPEM string, options *x509.VerifyOptions) (*x509.Certificate, error) {
165
+	var data *pem.Block
166
+	for remaining := []byte(certPEM); len(remaining) > 0; {
167
+		block, rest := pem.Decode(remaining)
168
+		if block == nil {
169
+			return nil, fmt.Errorf("error decoding certificate data")
170
+		}
171
+		if block.Type == "CERTIFICATE" {
172
+			data = block
173
+			break
174
+		}
175
+		remaining = rest
176
+	}
177
+
178
+	if data == nil || len(data.Bytes) < 1 {
179
+		return nil, fmt.Errorf("invalid/empty certificate data")
180
+	}
181
+
182
+	cert, err := x509.ParseCertificate(data.Bytes)
183
+	if err != nil {
184
+		return nil, fmt.Errorf("error parsing certificate: %s", err.Error())
185
+	}
186
+
187
+	if options != nil {
188
+		_, err = cert.Verify(*options)
189
+		if err != nil {
190
+			return cert, fmt.Errorf("error verifying certificate: %s", err.Error())
191
+		}
192
+	}
193
+
194
+	return cert, nil
195
+}
... ...
@@ -9,6 +9,109 @@ import (
9 9
 	"github.com/openshift/origin/pkg/route/api"
10 10
 )
11 11
 
12
+const (
13
+	testExpiredCAUnknownCertificate = `-----BEGIN CERTIFICATE-----
14
+MIIDIjCCAgqgAwIBAgIBBjANBgkqhkiG9w0BAQUFADCBoTELMAkGA1UEBhMCVVMx
15
+CzAJBgNVBAgMAlNDMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl
16
+ZmF1bHQgQ29tcGFueSBMdGQxEDAOBgNVBAsMB1Rlc3QgQ0ExGjAYBgNVBAMMEXd3
17
+dy5leGFtcGxlY2EuY29tMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1wbGUu
18
+Y29tMB4XDTE2MDExMzE5NDA1N1oXDTI2MDExMDE5NDA1N1owfDEYMBYGA1UEAxMP
19
+d3d3LmV4YW1wbGUuY29tMQswCQYDVQQIEwJTQzELMAkGA1UEBhMCVVMxIjAgBgkq
20
+hkiG9w0BCQEWE2V4YW1wbGVAZXhhbXBsZS5jb20xEDAOBgNVBAoTB0V4YW1wbGUx
21
+EDAOBgNVBAsTB0V4YW1wbGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM0B
22
+u++oHV1wcphWRbMLUft8fD7nPG95xs7UeLPphFZuShIhhdAQMpvcsFeg+Bg9PWCu
23
+v3jZljmk06MLvuWLfwjYfo9q/V+qOZVfTVHHbaIO5RTXJMC2Nn+ACF0kHBmNcbth
24
+OOgF8L854a/P8tjm1iPR++vHnkex0NH7lyosVc/vAgMBAAGjDTALMAkGA1UdEwQC
25
+MAAwDQYJKoZIhvcNAQEFBQADggEBADjFm5AlNH3DNT1Uzx3m66fFjqqrHEs25geT
26
+yA3rvBuynflEHQO95M/8wCxYVyuAx4Z1i4YDC7tx0vmOn/2GXZHY9MAj1I8KCnwt
27
+Jik7E2r1/yY0MrkawljOAxisXs821kJ+Z/51Ud2t5uhGxS6hJypbGspMS7OtBbw7
28
+8oThK7cWtCXOldNF6ruqY1agWnhRdAq5qSMnuBXuicOP0Kbtx51a1ugE3SnvQenJ
29
+nZxdtYUXvEsHZC/6bAtTfNh+/SwgxQJuL2ZM+VG3X2JIKY8xTDui+il7uTh422lq
30
+wED8uwKl+bOj6xFDyw4gWoBxRobsbFaME8pkykP1+GnKDberyAM=
31
+-----END CERTIFICATE-----`
32
+
33
+	testExpiredCertPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
34
+MIICWwIBAAKBgQDNAbvvqB1dcHKYVkWzC1H7fHw+5zxvecbO1Hiz6YRWbkoSIYXQ
35
+EDKb3LBXoPgYPT1grr942ZY5pNOjC77li38I2H6Pav1fqjmVX01Rx22iDuUU1yTA
36
+tjZ/gAhdJBwZjXG7YTjoBfC/OeGvz/LY5tYj0fvrx55HsdDR+5cqLFXP7wIDAQAB
37
+AoGAfE7P4Zsj6zOzGPI/Izj7Bi5OvGnEeKfzyBiH9Dflue74VRQkqqwXs/DWsNv3
38
+c+M2Y3iyu5ncgKmUduo5X8D9To2ymPRLGuCdfZTxnBMpIDKSJ0FTwVPkr6cYyyBk
39
+5VCbc470pQPxTAAtl2eaO1sIrzR4PcgwqrSOjwBQQocsGAECQQD8QOra/mZmxPbt
40
+bRh8U5lhgZmirImk5RY3QMPI/1/f4k+fyjkU5FRq/yqSyin75aSAXg8IupAFRgyZ
41
+W7BT6zwBAkEA0A0ugAGorpCbuTa25SsIOMxkEzCiKYvh0O+GfGkzWG4lkSeJqGME
42
+keuJGlXrZNKNoCYLluAKLPmnd72X2yTL7wJARM0kAXUP0wn324w8+HQIyqqBj/gF
43
+Vt9Q7uMQQ3s72CGu3ANZDFS2nbRZFU5koxrggk6lRRk1fOq9NvrmHg10AQJABOea
44
+pgfj+yGLmkUw8JwgGH6xCUbHO+WBUFSlPf+Y50fJeO+OrjqPXAVKeSV3ZCwWjKT4
45
+9viXJNJJ4WfF0bO/XwJAOMB1wQnEOSZ4v+laMwNtMq6hre5K8woqteXICoGcIWe8
46
+u3YLAbyW/lHhOCiZu2iAI8AbmXem9lW6Tr7p/97s0w==
47
+-----END RSA PRIVATE KEY-----`
48
+
49
+	testCertificate = `-----BEGIN CERTIFICATE-----
50
+MIICwjCCAiugAwIBAgIBATANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEL
51
+MAkGA1UECAwCQ0ExETAPBgNVBAoMCFNlY3VyaXR5MRswGQYDVQQLDBJPcGVuU2hp
52
+ZnQzIHRlc3QgQ0ExFzAVBgNVBAMMDmhlYWRlci50ZXN0IENBMB4XDTE2MDMxMjA0
53
+MjEwM1oXDTM2MDMxMjA0MjEwM1owWDEUMBIGA1UEAwwLaGVhZGVyLnRlc3QxCzAJ
54
+BgNVBAgMAkNBMQswCQYDVQQGEwJVUzERMA8GA1UECgwIU2VjdXJpdHkxEzARBgNV
55
+BAsMCk9wZW5TaGlmdDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0
56
+XEAzUMflZy8zluwzqMKnu8jYK3yUoEGLN0Bw0A/7ydno1g0E92ee8M9p59TCCWA6
57
+nKnt1DEK5285xAKs9AveutSYiDkpf2px59GvCVx2ecfFBTECWHMAJ/6Y7pqlWOt2
58
+hvPx5rP+jVeNLAfK9d+f57FGvWXrQAcBnFTegS6J910kbvDgNP4Nerj6RPAx2UOq
59
+6URqA4j7qZs63nReeu/1t//BQHNokKddfxw2ZXcL/5itgpPug16thp+ugGVdjcFs
60
+aasLJOjErUS0D+7bot98FL0TSpxWqwtCF117bSLY7UczZFNAZAOnZBFmSZBxcJJa
61
+TZzkda0Oiqo0J3GPcZ+rAgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQEL
62
+BQADgYEACkdKRUm9ERjgbe6w0fw4VY1s5XC9qR1m5AwLMVVwKxHJVG2zMzeDTHyg
63
+3cjxmfZdFU9yxmNUCh3mRsi2+qjEoFfGRyMwMMx7cduYhsFY3KA+Fl4vBRXAuPLR
64
+eCI4ErCPi+Y08vOto9VVXg2f4YFQYLq1X6TiXD5RpQAN0t8AYk4=
65
+-----END CERTIFICATE-----`
66
+
67
+	testPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
68
+MIIEpAIBAAKCAQEA9FxAM1DH5WcvM5bsM6jCp7vI2Ct8lKBBizdAcNAP+8nZ6NYN
69
+BPdnnvDPaefUwglgOpyp7dQxCudvOcQCrPQL3rrUmIg5KX9qcefRrwlcdnnHxQUx
70
+AlhzACf+mO6apVjrdobz8eaz/o1XjSwHyvXfn+exRr1l60AHAZxU3oEuifddJG7w
71
+4DT+DXq4+kTwMdlDqulEagOI+6mbOt50Xnrv9bf/wUBzaJCnXX8cNmV3C/+YrYKT
72
+7oNerYafroBlXY3BbGmrCyToxK1EtA/u26LffBS9E0qcVqsLQhdde20i2O1HM2RT
73
+QGQDp2QRZkmQcXCSWk2c5HWtDoqqNCdxj3GfqwIDAQABAoIBAEfl+NHge+CIur+w
74
+MXGFvziBLThFm1NTz9U5fZFz9q/8FUzH5m7GqMuASVb86oHpJlI4lFsw6vktXXGe
75
+tbbT28Y+LJ1wv3jxT42SSwT4eSc278uNmnz5L2UlX2j6E7CA+E8YqCBN5DoKtm8I
76
+PIbAT3sKPgP1aE6OuUEFEYeidOIMvjco2aQH0338sl6cObkQFEgnWf2ncun3KGnb
77
+s+dMO5EdYLo0rOdDXY88sElfqiNYYl/FRu9O3OfqHvScA5uo9FlIhukcrRkbjFcq
78
+j/7k4tt0iLs9B2j+4ihBWYo5eRFIde4Izj6a6ArEk0ShEUvwlZBuGMM/vs+jvbDK
79
+l3+0NpECgYEA/+qxwvOGjmlYNKFK/rzxd51EnfCISnV+tb17pNyRmlGToi1/LmmV
80
++jcJfcwlf2o8mTFn3xAdD3fSaHF7t8Li7xDwH2S+sSuFE/8bhgHUvw1S7oILMYyO
81
+hO6sWG+JocMhr8IejaAnQxav9VvP01YDfw/XBB0O1EIuzzr2KHq+AGMCgYEA9HCY
82
+JGTcv7lfs3kcCAkDtjl8NbjNRMxRErG0dfYS+6OSaXOOMg1TsaSNEgjOGyUX+yQ4
83
+4vtKcLwHk7+qz3ZPbhS6m7theZG9jUwMrQRGyCE7z3JUy8vmV/N+HP0V+boT+4KM
84
+Tai3+I3hf9+QMHYx/Z/VA0K6f27LwP+kEL9C8hkCgYEAoiHeXNRL+w1ihHVrPdgW
85
+YuGQBz/MGOA3VoylON1Eoa/tCGIqoQzjp5IWwUwEtaRon+VdGUTsJFCVTPYYm2Ms
86
+wqjIeBsrdLNNrE2C8nNWhXO7hr98t/eEk1NifOStHX6yaNdi4/cC6M4GzDtOf2WO
87
+8YDniAOg0Xjcjw2bxil9FmECgYBuUeq4cjUW6okArSYzki30rhka/d7WsAffEgjK
88
+PFbw7zADG74PZOhjAksQ2px6r9EU7ZInDxbXrmUVD6n9m/3ZRs25v2YMwfP0s1/9
89
+LjLr2+PsikMu/0VkaGaAmtCyNoMSPicoXX86VH5zgejHlnCVcO9oW1NkdBLNdhML
90
+4+ZI8QKBgQDb+SH7i50Yu3adwvPkDSp3ACCzPoHXno79a7Y5S2JzpFtNq+cNLWEb
91
+HP8gHJSZnaGrLKmjwNeQNsARYajKmDKO5HJ9g5H5Hae8enOb2yie541dneDT8rID
92
+4054dMQJnijd8620yf8wiNy05ZPOQQ0JvA/rW3WWZc5PGm8c2PsVjg==
93
+-----END RSA PRIVATE KEY-----`
94
+
95
+	testCACertificate = `-----BEGIN CERTIFICATE-----
96
+MIIClDCCAf2gAwIBAgIJAPU57OGhuqJtMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV
97
+BAYTAlVTMQswCQYDVQQIDAJDQTERMA8GA1UECgwIU2VjdXJpdHkxGzAZBgNVBAsM
98
+Ek9wZW5TaGlmdDMgdGVzdCBDQTEXMBUGA1UEAwwOaGVhZGVyLnRlc3QgQ0EwHhcN
99
+MTYwMzEyMDQyMTAzWhcNMzYwMzEyMDQyMTAzWjBjMQswCQYDVQQGEwJVUzELMAkG
100
+A1UECAwCQ0ExETAPBgNVBAoMCFNlY3VyaXR5MRswGQYDVQQLDBJPcGVuU2hpZnQz
101
+IHRlc3QgQ0ExFzAVBgNVBAMMDmhlYWRlci50ZXN0IENBMIGfMA0GCSqGSIb3DQEB
102
+AQUAA4GNADCBiQKBgQCsdVIJ6GSrkFdE9LzsMItYGE4q3qqSqIbs/uwMoVsMT+33
103
+pLeyzeecPuoQsdO6SEuqhUM1ivUN4GyXIR1+aW2baMwMXpjX9VIJu5d4FqtGi6SD
104
+RfV+tbERWwifPJlN+ryuvqbbDxrjQeXhemeo7yrJdgJ1oyDmoM5pTiSUUmltvQID
105
+AQABo1AwTjAdBgNVHQ4EFgQUOVuieqGfp2wnKo7lX2fQt+Yk1C4wHwYDVR0jBBgw
106
+FoAUOVuieqGfp2wnKo7lX2fQt+Yk1C4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
107
+AQsFAAOBgQA8VhmNeicRnKgXInVyYZDjL0P4WRbKJY7DkJxRMRWxikbEVHdySki6
108
+jegpqgJqYbzU6EiuTS2sl2bAjIK9nGUtTDt1PJIC1Evn5Q6v5ylNflpv6GxtUbCt
109
+bGvtpjWA4r9WASIDPFsxk/cDEEEO6iPxgMOf5MdpQC2y2MU0rzF/Gg==
110
+-----END CERTIFICATE-----`
111
+
112
+	testDestinationCACertificate = testCACertificate
113
+)
114
+
12 115
 // TestValidateRouteBad ensures not specifying a required field results in error and a fully specified
13 116
 // route passes successfully
14 117
 func TestValidateRoute(t *testing.T) {
... ...
@@ -429,6 +532,17 @@ func TestValidateTLSInsecureEdgeTerminationPolicy(t *testing.T) {
429 429
 				},
430 430
 			},
431 431
 		},
432
+		{
433
+			name: "Reencrypt termination DestCACert",
434
+			route: &api.Route{
435
+				Spec: api.RouteSpec{
436
+					TLS: &api.TLSConfig{
437
+						Termination:              api.TLSTerminationReencrypt,
438
+						DestinationCACertificate: testDestinationCACertificate,
439
+					},
440
+				},
441
+			},
442
+		},
432 443
 	}
433 444
 
434 445
 	insecureTypes := []api.InsecureEdgeTerminationPolicyType{
... ...
@@ -544,3 +658,289 @@ func TestValidateNoTLSInsecureEdgeTerminationPolicy(t *testing.T) {
544 544
 		}
545 545
 	}
546 546
 }
547
+
548
+// TestExtendedValidateRoute ensures that a route's certificate and keys
549
+// are valid.
550
+func TestExtendedValidateRoute(t *testing.T) {
551
+	tests := []struct {
552
+		name           string
553
+		route          *api.Route
554
+		expectedErrors int
555
+	}{
556
+		{
557
+			name: "No TLS Termination",
558
+			route: &api.Route{
559
+				Spec: api.RouteSpec{
560
+					TLS: &api.TLSConfig{
561
+						Termination: "",
562
+					},
563
+				},
564
+			},
565
+			expectedErrors: 1,
566
+		},
567
+		{
568
+			name: "Passthrough termination OK",
569
+			route: &api.Route{
570
+				Spec: api.RouteSpec{
571
+					TLS: &api.TLSConfig{
572
+						Termination: api.TLSTerminationPassthrough,
573
+					},
574
+				},
575
+			},
576
+			expectedErrors: 0,
577
+		},
578
+		{
579
+			name: "Reencrypt termination OK with certs",
580
+			route: &api.Route{
581
+				Spec: api.RouteSpec{
582
+					Host: "www.example.com",
583
+
584
+					TLS: &api.TLSConfig{
585
+						Termination:              api.TLSTerminationReencrypt,
586
+						Certificate:              testCertificate,
587
+						Key:                      testPrivateKey,
588
+						CACertificate:            testCACertificate,
589
+						DestinationCACertificate: testDestinationCACertificate,
590
+					},
591
+				},
592
+			},
593
+			expectedErrors: 0,
594
+		},
595
+		{
596
+			name: "Reencrypt termination OK with bad config",
597
+			route: &api.Route{
598
+				Spec: api.RouteSpec{
599
+					TLS: &api.TLSConfig{
600
+						Termination:              api.TLSTerminationReencrypt,
601
+						Certificate:              "def",
602
+						Key:                      "ghi",
603
+						CACertificate:            "jkl",
604
+						DestinationCACertificate: "abc",
605
+					},
606
+				},
607
+			},
608
+			expectedErrors: 4,
609
+		},
610
+		{
611
+			name: "Reencrypt termination OK without certs",
612
+			route: &api.Route{
613
+				Spec: api.RouteSpec{
614
+					TLS: &api.TLSConfig{
615
+						Termination:              api.TLSTerminationReencrypt,
616
+						DestinationCACertificate: testDestinationCACertificate,
617
+					},
618
+				},
619
+			},
620
+			expectedErrors: 0,
621
+		},
622
+		{
623
+			name: "Reencrypt termination bad config without certs",
624
+			route: &api.Route{
625
+				Spec: api.RouteSpec{
626
+					TLS: &api.TLSConfig{
627
+						Termination:              api.TLSTerminationReencrypt,
628
+						DestinationCACertificate: "abc",
629
+					},
630
+				},
631
+			},
632
+			expectedErrors: 1,
633
+		},
634
+		{
635
+			name: "Reencrypt termination no dest cert",
636
+			route: &api.Route{
637
+				Spec: api.RouteSpec{
638
+					Host: "www.example.com",
639
+					TLS: &api.TLSConfig{
640
+						Termination:   api.TLSTerminationReencrypt,
641
+						Certificate:   testCertificate,
642
+						Key:           testPrivateKey,
643
+						CACertificate: testCACertificate,
644
+					},
645
+				},
646
+			},
647
+			expectedErrors: 1,
648
+		},
649
+		{
650
+			name: "Edge termination OK with certs without host",
651
+			route: &api.Route{
652
+				Spec: api.RouteSpec{
653
+					TLS: &api.TLSConfig{
654
+						Termination:   api.TLSTerminationEdge,
655
+						Certificate:   testCertificate,
656
+						Key:           testPrivateKey,
657
+						CACertificate: testCACertificate,
658
+					},
659
+				},
660
+			},
661
+			expectedErrors: 0,
662
+		},
663
+		{
664
+			name: "Edge termination OK with certs",
665
+			route: &api.Route{
666
+				Spec: api.RouteSpec{
667
+					Host: "www.example.com",
668
+					TLS: &api.TLSConfig{
669
+						Termination:   api.TLSTerminationEdge,
670
+						Certificate:   testCertificate,
671
+						Key:           testPrivateKey,
672
+						CACertificate: testCACertificate,
673
+					},
674
+				},
675
+			},
676
+			expectedErrors: 0,
677
+		},
678
+		{
679
+			name: "Edge termination bad config with certs",
680
+			route: &api.Route{
681
+				Spec: api.RouteSpec{
682
+					Host: "www.example.com",
683
+					TLS: &api.TLSConfig{
684
+						Termination:   api.TLSTerminationEdge,
685
+						Certificate:   "abc",
686
+						Key:           "abc",
687
+						CACertificate: "abc",
688
+					},
689
+				},
690
+			},
691
+			expectedErrors: 3,
692
+		},
693
+		{
694
+			name: "Edge termination mismatched key and cert",
695
+			route: &api.Route{
696
+				Spec: api.RouteSpec{
697
+					Host: "www.example.com",
698
+					TLS: &api.TLSConfig{
699
+						Termination:   api.TLSTerminationEdge,
700
+						Certificate:   testCertificate,
701
+						Key:           testExpiredCertPrivateKey,
702
+						CACertificate: testCACertificate,
703
+					},
704
+				},
705
+			},
706
+			expectedErrors: 1,
707
+		},
708
+		{
709
+			name: "Edge termination expired cert",
710
+			route: &api.Route{
711
+				Spec: api.RouteSpec{
712
+					Host: "www.example.com",
713
+					TLS: &api.TLSConfig{
714
+						Termination:   api.TLSTerminationEdge,
715
+						Certificate:   testExpiredCAUnknownCertificate,
716
+						Key:           testExpiredCertPrivateKey,
717
+						CACertificate: testCACertificate,
718
+					},
719
+				},
720
+			},
721
+			expectedErrors: 1,
722
+		},
723
+		{
724
+			name: "Edge termination expired cert key mismatch",
725
+			route: &api.Route{
726
+				Spec: api.RouteSpec{
727
+					Host: "www.example.com",
728
+					TLS: &api.TLSConfig{
729
+						Termination:   api.TLSTerminationEdge,
730
+						Certificate:   testExpiredCAUnknownCertificate,
731
+						Key:           testPrivateKey,
732
+						CACertificate: testCACertificate,
733
+					},
734
+				},
735
+			},
736
+			expectedErrors: 2,
737
+		},
738
+		{
739
+			name: "Edge termination OK without certs",
740
+			route: &api.Route{
741
+				Spec: api.RouteSpec{
742
+					TLS: &api.TLSConfig{
743
+						Termination: api.TLSTerminationEdge,
744
+					},
745
+				},
746
+			},
747
+			expectedErrors: 0,
748
+		},
749
+		{
750
+			name: "Edge termination, dest cert",
751
+			route: &api.Route{
752
+				Spec: api.RouteSpec{
753
+					TLS: &api.TLSConfig{
754
+						Termination:              api.TLSTerminationEdge,
755
+						DestinationCACertificate: "abc",
756
+					},
757
+				},
758
+			},
759
+			expectedErrors: 2,
760
+		},
761
+		{
762
+			name: "Passthrough termination, cert",
763
+			route: &api.Route{
764
+				Spec: api.RouteSpec{
765
+					TLS: &api.TLSConfig{Termination: api.TLSTerminationPassthrough, Certificate: "test"},
766
+				},
767
+			},
768
+			expectedErrors: 3,
769
+		},
770
+		{
771
+			name: "Passthrough termination, key",
772
+			route: &api.Route{
773
+				Spec: api.RouteSpec{
774
+					TLS: &api.TLSConfig{Termination: api.TLSTerminationPassthrough, Key: "test"},
775
+				},
776
+			},
777
+			expectedErrors: 1,
778
+		},
779
+		{
780
+			name: "Passthrough termination, ca cert",
781
+			route: &api.Route{
782
+				Spec: api.RouteSpec{
783
+					TLS: &api.TLSConfig{Termination: api.TLSTerminationPassthrough, CACertificate: "test"},
784
+				},
785
+			},
786
+			expectedErrors: 2,
787
+		},
788
+		{
789
+			name: "Passthrough termination, dest ca cert",
790
+			route: &api.Route{
791
+				Spec: api.RouteSpec{
792
+					TLS: &api.TLSConfig{Termination: api.TLSTerminationPassthrough, DestinationCACertificate: "test"},
793
+				},
794
+			},
795
+			expectedErrors: 2,
796
+		},
797
+		{
798
+			name: "Invalid termination type",
799
+			route: &api.Route{
800
+				Spec: api.RouteSpec{
801
+					TLS: &api.TLSConfig{
802
+						Termination: "invalid",
803
+					},
804
+				},
805
+			},
806
+			expectedErrors: 1,
807
+		},
808
+		{
809
+			name: "Double escaped newlines",
810
+			route: &api.Route{
811
+				Spec: api.RouteSpec{
812
+					TLS: &api.TLSConfig{
813
+						Termination:              api.TLSTerminationReencrypt,
814
+						Certificate:              "d\\nef",
815
+						Key:                      "g\\nhi",
816
+						CACertificate:            "j\\nkl",
817
+						DestinationCACertificate: "j\\nkl",
818
+					},
819
+				},
820
+			},
821
+			expectedErrors: 4,
822
+		},
823
+	}
824
+
825
+	for _, tc := range tests {
826
+		errs := ExtendedValidateRoute(tc.route)
827
+
828
+		if len(errs) != tc.expectedErrors {
829
+			t.Errorf("Test case %s expected %d error(s), got %d. %v", tc.name, tc.expectedErrors, len(errs), errs)
830
+		}
831
+	}
832
+}
547 833
new file mode 100644
... ...
@@ -0,0 +1,75 @@
0
+package controller
1
+
2
+import (
3
+	"fmt"
4
+	"reflect"
5
+
6
+	"github.com/golang/glog"
7
+	kapi "k8s.io/kubernetes/pkg/api"
8
+	"k8s.io/kubernetes/pkg/util/sets"
9
+	"k8s.io/kubernetes/pkg/watch"
10
+
11
+	routeapi "github.com/openshift/origin/pkg/route/api"
12
+	"github.com/openshift/origin/pkg/route/api/validation"
13
+	"github.com/openshift/origin/pkg/router"
14
+)
15
+
16
+// ExtendedValidator implements the router.Plugin interface to provide
17
+// extended config validation for template based, backend-agnostic routers.
18
+type ExtendedValidator struct {
19
+	// plugin is the next plugin in the chain.
20
+	plugin router.Plugin
21
+
22
+	// recorder is an interface for indicating route rejections.
23
+	recorder RejectionRecorder
24
+
25
+	// invalidRoutes is a map of invalid routes previously encountered.
26
+	invalidRoutes map[string]routeapi.Route
27
+}
28
+
29
+// ExtendedValidator creates a plugin wrapper that ensures only routes that
30
+// pass extended validation are relayed to the next plugin in the chain.
31
+// Recorder is an interface for indicating why a route was rejected.
32
+func NewExtendedValidator(plugin router.Plugin, recorder RejectionRecorder) *ExtendedValidator {
33
+	return &ExtendedValidator{
34
+		plugin:        plugin,
35
+		recorder:      recorder,
36
+		invalidRoutes: make(map[string]routeapi.Route),
37
+	}
38
+}
39
+
40
+// HandleEndpoints processes watch events on the Endpoints resource.
41
+func (p *ExtendedValidator) HandleEndpoints(eventType watch.EventType, endpoints *kapi.Endpoints) error {
42
+	return p.plugin.HandleEndpoints(eventType, endpoints)
43
+}
44
+
45
+// HandleRoute processes watch events on the Route resource.
46
+func (p *ExtendedValidator) HandleRoute(eventType watch.EventType, route *routeapi.Route) error {
47
+	// Check if previously seen route and its Spec is unchanged.
48
+	routeName := routeNameKey(route)
49
+	old, ok := p.invalidRoutes[routeName]
50
+	if ok && reflect.DeepEqual(old.Spec, route.Spec) {
51
+		// Route spec was unchanged and it is already marked in
52
+		// error, we don't need to do anything more.
53
+		return fmt.Errorf("invalid route configuration")
54
+	}
55
+
56
+	if errs := validation.ExtendedValidateRoute(route); len(errs) > 0 {
57
+		errmsg := ""
58
+		for i := 0; i < len(errs); i++ {
59
+			errmsg = errmsg + "\n  - " + errs[i].Error()
60
+		}
61
+		glog.Errorf("Skipping route %s due to invalid configuration: %s", routeName, errmsg)
62
+
63
+		p.recorder.RecordRouteRejection(route, "ExtendedValidationFailed", errmsg)
64
+		return fmt.Errorf("invalid route configuration")
65
+	}
66
+
67
+	return p.plugin.HandleRoute(eventType, route)
68
+}
69
+
70
+// HandleAllowedNamespaces limits the scope of valid routes to only those that match
71
+// the provided namespace list.
72
+func (p *ExtendedValidator) HandleNamespaces(namespaces sets.String) error {
73
+	return p.plugin.HandleNamespaces(namespaces)
74
+}
... ...
@@ -16,6 +16,109 @@ import (
16 16
 	"github.com/openshift/origin/pkg/router/controller"
17 17
 )
18 18
 
19
+const (
20
+	testExpiredCAUnknownCertificate = `-----BEGIN CERTIFICATE-----
21
+MIIDIjCCAgqgAwIBAgIBBjANBgkqhkiG9w0BAQUFADCBoTELMAkGA1UEBhMCVVMx
22
+CzAJBgNVBAgMAlNDMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl
23
+ZmF1bHQgQ29tcGFueSBMdGQxEDAOBgNVBAsMB1Rlc3QgQ0ExGjAYBgNVBAMMEXd3
24
+dy5leGFtcGxlY2EuY29tMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1wbGUu
25
+Y29tMB4XDTE2MDExMzE5NDA1N1oXDTI2MDExMDE5NDA1N1owfDEYMBYGA1UEAxMP
26
+d3d3LmV4YW1wbGUuY29tMQswCQYDVQQIEwJTQzELMAkGA1UEBhMCVVMxIjAgBgkq
27
+hkiG9w0BCQEWE2V4YW1wbGVAZXhhbXBsZS5jb20xEDAOBgNVBAoTB0V4YW1wbGUx
28
+EDAOBgNVBAsTB0V4YW1wbGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM0B
29
+u++oHV1wcphWRbMLUft8fD7nPG95xs7UeLPphFZuShIhhdAQMpvcsFeg+Bg9PWCu
30
+v3jZljmk06MLvuWLfwjYfo9q/V+qOZVfTVHHbaIO5RTXJMC2Nn+ACF0kHBmNcbth
31
+OOgF8L854a/P8tjm1iPR++vHnkex0NH7lyosVc/vAgMBAAGjDTALMAkGA1UdEwQC
32
+MAAwDQYJKoZIhvcNAQEFBQADggEBADjFm5AlNH3DNT1Uzx3m66fFjqqrHEs25geT
33
+yA3rvBuynflEHQO95M/8wCxYVyuAx4Z1i4YDC7tx0vmOn/2GXZHY9MAj1I8KCnwt
34
+Jik7E2r1/yY0MrkawljOAxisXs821kJ+Z/51Ud2t5uhGxS6hJypbGspMS7OtBbw7
35
+8oThK7cWtCXOldNF6ruqY1agWnhRdAq5qSMnuBXuicOP0Kbtx51a1ugE3SnvQenJ
36
+nZxdtYUXvEsHZC/6bAtTfNh+/SwgxQJuL2ZM+VG3X2JIKY8xTDui+il7uTh422lq
37
+wED8uwKl+bOj6xFDyw4gWoBxRobsbFaME8pkykP1+GnKDberyAM=
38
+-----END CERTIFICATE-----`
39
+
40
+	testExpiredCertPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
41
+MIICWwIBAAKBgQDNAbvvqB1dcHKYVkWzC1H7fHw+5zxvecbO1Hiz6YRWbkoSIYXQ
42
+EDKb3LBXoPgYPT1grr942ZY5pNOjC77li38I2H6Pav1fqjmVX01Rx22iDuUU1yTA
43
+tjZ/gAhdJBwZjXG7YTjoBfC/OeGvz/LY5tYj0fvrx55HsdDR+5cqLFXP7wIDAQAB
44
+AoGAfE7P4Zsj6zOzGPI/Izj7Bi5OvGnEeKfzyBiH9Dflue74VRQkqqwXs/DWsNv3
45
+c+M2Y3iyu5ncgKmUduo5X8D9To2ymPRLGuCdfZTxnBMpIDKSJ0FTwVPkr6cYyyBk
46
+5VCbc470pQPxTAAtl2eaO1sIrzR4PcgwqrSOjwBQQocsGAECQQD8QOra/mZmxPbt
47
+bRh8U5lhgZmirImk5RY3QMPI/1/f4k+fyjkU5FRq/yqSyin75aSAXg8IupAFRgyZ
48
+W7BT6zwBAkEA0A0ugAGorpCbuTa25SsIOMxkEzCiKYvh0O+GfGkzWG4lkSeJqGME
49
+keuJGlXrZNKNoCYLluAKLPmnd72X2yTL7wJARM0kAXUP0wn324w8+HQIyqqBj/gF
50
+Vt9Q7uMQQ3s72CGu3ANZDFS2nbRZFU5koxrggk6lRRk1fOq9NvrmHg10AQJABOea
51
+pgfj+yGLmkUw8JwgGH6xCUbHO+WBUFSlPf+Y50fJeO+OrjqPXAVKeSV3ZCwWjKT4
52
+9viXJNJJ4WfF0bO/XwJAOMB1wQnEOSZ4v+laMwNtMq6hre5K8woqteXICoGcIWe8
53
+u3YLAbyW/lHhOCiZu2iAI8AbmXem9lW6Tr7p/97s0w==
54
+-----END RSA PRIVATE KEY-----`
55
+
56
+	testCertificate = `-----BEGIN CERTIFICATE-----
57
+MIICwjCCAiugAwIBAgIBATANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEL
58
+MAkGA1UECAwCQ0ExETAPBgNVBAoMCFNlY3VyaXR5MRswGQYDVQQLDBJPcGVuU2hp
59
+ZnQzIHRlc3QgQ0ExFzAVBgNVBAMMDmhlYWRlci50ZXN0IENBMB4XDTE2MDMxMjA0
60
+MjEwM1oXDTM2MDMxMjA0MjEwM1owWDEUMBIGA1UEAwwLaGVhZGVyLnRlc3QxCzAJ
61
+BgNVBAgMAkNBMQswCQYDVQQGEwJVUzERMA8GA1UECgwIU2VjdXJpdHkxEzARBgNV
62
+BAsMCk9wZW5TaGlmdDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0
63
+XEAzUMflZy8zluwzqMKnu8jYK3yUoEGLN0Bw0A/7ydno1g0E92ee8M9p59TCCWA6
64
+nKnt1DEK5285xAKs9AveutSYiDkpf2px59GvCVx2ecfFBTECWHMAJ/6Y7pqlWOt2
65
+hvPx5rP+jVeNLAfK9d+f57FGvWXrQAcBnFTegS6J910kbvDgNP4Nerj6RPAx2UOq
66
+6URqA4j7qZs63nReeu/1t//BQHNokKddfxw2ZXcL/5itgpPug16thp+ugGVdjcFs
67
+aasLJOjErUS0D+7bot98FL0TSpxWqwtCF117bSLY7UczZFNAZAOnZBFmSZBxcJJa
68
+TZzkda0Oiqo0J3GPcZ+rAgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQEL
69
+BQADgYEACkdKRUm9ERjgbe6w0fw4VY1s5XC9qR1m5AwLMVVwKxHJVG2zMzeDTHyg
70
+3cjxmfZdFU9yxmNUCh3mRsi2+qjEoFfGRyMwMMx7cduYhsFY3KA+Fl4vBRXAuPLR
71
+eCI4ErCPi+Y08vOto9VVXg2f4YFQYLq1X6TiXD5RpQAN0t8AYk4=
72
+-----END CERTIFICATE-----`
73
+
74
+	testPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
75
+MIIEpAIBAAKCAQEA9FxAM1DH5WcvM5bsM6jCp7vI2Ct8lKBBizdAcNAP+8nZ6NYN
76
+BPdnnvDPaefUwglgOpyp7dQxCudvOcQCrPQL3rrUmIg5KX9qcefRrwlcdnnHxQUx
77
+AlhzACf+mO6apVjrdobz8eaz/o1XjSwHyvXfn+exRr1l60AHAZxU3oEuifddJG7w
78
+4DT+DXq4+kTwMdlDqulEagOI+6mbOt50Xnrv9bf/wUBzaJCnXX8cNmV3C/+YrYKT
79
+7oNerYafroBlXY3BbGmrCyToxK1EtA/u26LffBS9E0qcVqsLQhdde20i2O1HM2RT
80
+QGQDp2QRZkmQcXCSWk2c5HWtDoqqNCdxj3GfqwIDAQABAoIBAEfl+NHge+CIur+w
81
+MXGFvziBLThFm1NTz9U5fZFz9q/8FUzH5m7GqMuASVb86oHpJlI4lFsw6vktXXGe
82
+tbbT28Y+LJ1wv3jxT42SSwT4eSc278uNmnz5L2UlX2j6E7CA+E8YqCBN5DoKtm8I
83
+PIbAT3sKPgP1aE6OuUEFEYeidOIMvjco2aQH0338sl6cObkQFEgnWf2ncun3KGnb
84
+s+dMO5EdYLo0rOdDXY88sElfqiNYYl/FRu9O3OfqHvScA5uo9FlIhukcrRkbjFcq
85
+j/7k4tt0iLs9B2j+4ihBWYo5eRFIde4Izj6a6ArEk0ShEUvwlZBuGMM/vs+jvbDK
86
+l3+0NpECgYEA/+qxwvOGjmlYNKFK/rzxd51EnfCISnV+tb17pNyRmlGToi1/LmmV
87
++jcJfcwlf2o8mTFn3xAdD3fSaHF7t8Li7xDwH2S+sSuFE/8bhgHUvw1S7oILMYyO
88
+hO6sWG+JocMhr8IejaAnQxav9VvP01YDfw/XBB0O1EIuzzr2KHq+AGMCgYEA9HCY
89
+JGTcv7lfs3kcCAkDtjl8NbjNRMxRErG0dfYS+6OSaXOOMg1TsaSNEgjOGyUX+yQ4
90
+4vtKcLwHk7+qz3ZPbhS6m7theZG9jUwMrQRGyCE7z3JUy8vmV/N+HP0V+boT+4KM
91
+Tai3+I3hf9+QMHYx/Z/VA0K6f27LwP+kEL9C8hkCgYEAoiHeXNRL+w1ihHVrPdgW
92
+YuGQBz/MGOA3VoylON1Eoa/tCGIqoQzjp5IWwUwEtaRon+VdGUTsJFCVTPYYm2Ms
93
+wqjIeBsrdLNNrE2C8nNWhXO7hr98t/eEk1NifOStHX6yaNdi4/cC6M4GzDtOf2WO
94
+8YDniAOg0Xjcjw2bxil9FmECgYBuUeq4cjUW6okArSYzki30rhka/d7WsAffEgjK
95
+PFbw7zADG74PZOhjAksQ2px6r9EU7ZInDxbXrmUVD6n9m/3ZRs25v2YMwfP0s1/9
96
+LjLr2+PsikMu/0VkaGaAmtCyNoMSPicoXX86VH5zgejHlnCVcO9oW1NkdBLNdhML
97
+4+ZI8QKBgQDb+SH7i50Yu3adwvPkDSp3ACCzPoHXno79a7Y5S2JzpFtNq+cNLWEb
98
+HP8gHJSZnaGrLKmjwNeQNsARYajKmDKO5HJ9g5H5Hae8enOb2yie541dneDT8rID
99
+4054dMQJnijd8620yf8wiNy05ZPOQQ0JvA/rW3WWZc5PGm8c2PsVjg==
100
+-----END RSA PRIVATE KEY-----`
101
+
102
+	testCACertificate = `-----BEGIN CERTIFICATE-----
103
+MIIClDCCAf2gAwIBAgIJAPU57OGhuqJtMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV
104
+BAYTAlVTMQswCQYDVQQIDAJDQTERMA8GA1UECgwIU2VjdXJpdHkxGzAZBgNVBAsM
105
+Ek9wZW5TaGlmdDMgdGVzdCBDQTEXMBUGA1UEAwwOaGVhZGVyLnRlc3QgQ0EwHhcN
106
+MTYwMzEyMDQyMTAzWhcNMzYwMzEyMDQyMTAzWjBjMQswCQYDVQQGEwJVUzELMAkG
107
+A1UECAwCQ0ExETAPBgNVBAoMCFNlY3VyaXR5MRswGQYDVQQLDBJPcGVuU2hpZnQz
108
+IHRlc3QgQ0ExFzAVBgNVBAMMDmhlYWRlci50ZXN0IENBMIGfMA0GCSqGSIb3DQEB
109
+AQUAA4GNADCBiQKBgQCsdVIJ6GSrkFdE9LzsMItYGE4q3qqSqIbs/uwMoVsMT+33
110
+pLeyzeecPuoQsdO6SEuqhUM1ivUN4GyXIR1+aW2baMwMXpjX9VIJu5d4FqtGi6SD
111
+RfV+tbERWwifPJlN+ryuvqbbDxrjQeXhemeo7yrJdgJ1oyDmoM5pTiSUUmltvQID
112
+AQABo1AwTjAdBgNVHQ4EFgQUOVuieqGfp2wnKo7lX2fQt+Yk1C4wHwYDVR0jBBgw
113
+FoAUOVuieqGfp2wnKo7lX2fQt+Yk1C4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
114
+AQsFAAOBgQA8VhmNeicRnKgXInVyYZDjL0P4WRbKJY7DkJxRMRWxikbEVHdySki6
115
+jegpqgJqYbzU6EiuTS2sl2bAjIK9nGUtTDt1PJIC1Evn5Q6v5ylNflpv6GxtUbCt
116
+bGvtpjWA4r9WASIDPFsxk/cDEEEO6iPxgMOf5MdpQC2y2MU0rzF/Gg==
117
+-----END CERTIFICATE-----`
118
+
119
+	testDestinationCACertificate = testCACertificate
120
+)
121
+
19 122
 // TestRouter provides an implementation of the plugin's router interface suitable for unit testing.
20 123
 type TestRouter struct {
21 124
 	State     map[string]ServiceUnit
... ...
@@ -536,6 +639,360 @@ func TestHandleRoute(t *testing.T) {
536 536
 	}
537 537
 }
538 538
 
539
+// TestHandleRouteExtendedValidation test route watch events with extended route configuration validation.
540
+func TestHandleRouteExtendedValidation(t *testing.T) {
541
+	rejections := &fakeRejections{}
542
+	router := newTestRouter(make(map[string]ServiceUnit))
543
+	templatePlugin := newDefaultTemplatePlugin(router, true)
544
+	// TODO: move tests that rely on unique hosts to pkg/router/controller and remove them from
545
+	// here
546
+	extendedValidatorPlugin := controller.NewExtendedValidator(templatePlugin, rejections)
547
+	plugin := controller.NewUniqueHost(extendedValidatorPlugin, controller.HostForRoute, rejections)
548
+
549
+	original := unversioned.Time{Time: time.Now()}
550
+
551
+	//add
552
+	route := &routeapi.Route{
553
+		ObjectMeta: kapi.ObjectMeta{
554
+			CreationTimestamp: original,
555
+			Namespace:         "foo",
556
+			Name:              "test",
557
+		},
558
+		Spec: routeapi.RouteSpec{
559
+			Host: "www.example.com",
560
+			To: kapi.ObjectReference{
561
+				Name: "TestService",
562
+			},
563
+		},
564
+	}
565
+	serviceUnitKey := fmt.Sprintf("%s/%s", route.Namespace, route.Spec.To.Name)
566
+
567
+	plugin.HandleRoute(watch.Added, route)
568
+
569
+	if !router.Committed {
570
+		t.Errorf("Expected router to be committed after HandleRoute call")
571
+	}
572
+
573
+	actualSU, ok := router.FindServiceUnit(serviceUnitKey)
574
+
575
+	if !ok {
576
+		t.Errorf("TestHandleRoute was unable to find the service unit %s after HandleRoute was called", route.Spec.To.Name)
577
+	} else {
578
+		serviceAliasCfg, ok := actualSU.ServiceAliasConfigs[router.routeKey(route)]
579
+
580
+		if !ok {
581
+			t.Errorf("TestHandleRoute expected route key %s", router.routeKey(route))
582
+		} else {
583
+			if serviceAliasCfg.Host != route.Spec.Host || serviceAliasCfg.Path != route.Spec.Path {
584
+				t.Errorf("Expected route did not match service alias config %v : %v", route, serviceAliasCfg)
585
+			}
586
+		}
587
+	}
588
+
589
+	if len(rejections.rejections) > 0 {
590
+		t.Fatalf("did not expect a recorded rejection: %#v", rejections)
591
+	}
592
+
593
+	tests := []struct {
594
+		name          string
595
+		route         *routeapi.Route
596
+		errorExpected bool
597
+	}{
598
+		{
599
+			name: "No TLS Termination",
600
+			route: &routeapi.Route{
601
+				Spec: routeapi.RouteSpec{
602
+					Host: "www.no.tls.test",
603
+					TLS: &routeapi.TLSConfig{
604
+						Termination: "",
605
+					},
606
+				},
607
+			},
608
+			errorExpected: true,
609
+		},
610
+		{
611
+			name: "Passthrough termination OK",
612
+			route: &routeapi.Route{
613
+				Spec: routeapi.RouteSpec{
614
+					Host: "www.passthrough.test",
615
+					TLS: &routeapi.TLSConfig{
616
+						Termination: routeapi.TLSTerminationPassthrough,
617
+					},
618
+				},
619
+			},
620
+			errorExpected: false,
621
+		},
622
+		{
623
+			name: "Reencrypt termination OK with certs",
624
+			route: &routeapi.Route{
625
+				Spec: routeapi.RouteSpec{
626
+					Host: "www.example.com",
627
+
628
+					TLS: &routeapi.TLSConfig{
629
+						Termination:              routeapi.TLSTerminationReencrypt,
630
+						Certificate:              testCertificate,
631
+						Key:                      testPrivateKey,
632
+						CACertificate:            testCACertificate,
633
+						DestinationCACertificate: testDestinationCACertificate,
634
+					},
635
+				},
636
+			},
637
+			errorExpected: false,
638
+		},
639
+		{
640
+			name: "Reencrypt termination OK with bad config",
641
+			route: &routeapi.Route{
642
+				Spec: routeapi.RouteSpec{
643
+					Host: "www.reencypt.badconfig.test",
644
+					TLS: &routeapi.TLSConfig{
645
+						Termination:              routeapi.TLSTerminationReencrypt,
646
+						Certificate:              "def",
647
+						Key:                      "ghi",
648
+						CACertificate:            "jkl",
649
+						DestinationCACertificate: "abc",
650
+					},
651
+				},
652
+			},
653
+			errorExpected: true,
654
+		},
655
+		{
656
+			name: "Reencrypt termination OK without certs",
657
+			route: &routeapi.Route{
658
+				Spec: routeapi.RouteSpec{
659
+					Host: "www.reencypt.nocerts.test",
660
+					TLS: &routeapi.TLSConfig{
661
+						Termination:              routeapi.TLSTerminationReencrypt,
662
+						DestinationCACertificate: testDestinationCACertificate,
663
+					},
664
+				},
665
+			},
666
+			errorExpected: false,
667
+		},
668
+		{
669
+			name: "Reencrypt termination bad config without certs",
670
+			route: &routeapi.Route{
671
+				Spec: routeapi.RouteSpec{
672
+					Host: "www.reencypt.badconfignocerts.test",
673
+					TLS: &routeapi.TLSConfig{
674
+						Termination:              routeapi.TLSTerminationReencrypt,
675
+						DestinationCACertificate: "abc",
676
+					},
677
+				},
678
+			},
679
+			errorExpected: true,
680
+		},
681
+		{
682
+			name: "Reencrypt termination no dest cert",
683
+			route: &routeapi.Route{
684
+				Spec: routeapi.RouteSpec{
685
+					Host: "www.reencypt.nodestcert.test",
686
+					TLS: &routeapi.TLSConfig{
687
+						Termination:   routeapi.TLSTerminationReencrypt,
688
+						Certificate:   testCertificate,
689
+						Key:           testPrivateKey,
690
+						CACertificate: testCACertificate,
691
+					},
692
+				},
693
+			},
694
+			errorExpected: true,
695
+		},
696
+		{
697
+			name: "Edge termination OK with certs without host",
698
+			route: &routeapi.Route{
699
+				Spec: routeapi.RouteSpec{
700
+					TLS: &routeapi.TLSConfig{
701
+						Termination:   routeapi.TLSTerminationEdge,
702
+						Certificate:   testCertificate,
703
+						Key:           testPrivateKey,
704
+						CACertificate: testCACertificate,
705
+					},
706
+				},
707
+			},
708
+			errorExpected: false,
709
+		},
710
+		{
711
+			name: "Edge termination OK with certs",
712
+			route: &routeapi.Route{
713
+				Spec: routeapi.RouteSpec{
714
+					Host: "www.example.com",
715
+					TLS: &routeapi.TLSConfig{
716
+						Termination:   routeapi.TLSTerminationEdge,
717
+						Certificate:   testCertificate,
718
+						Key:           testPrivateKey,
719
+						CACertificate: testCACertificate,
720
+					},
721
+				},
722
+			},
723
+			errorExpected: false,
724
+		},
725
+		{
726
+			name: "Edge termination bad config with certs",
727
+			route: &routeapi.Route{
728
+				Spec: routeapi.RouteSpec{
729
+					Host: "www.edge.badconfig.test",
730
+					TLS: &routeapi.TLSConfig{
731
+						Termination:   routeapi.TLSTerminationEdge,
732
+						Certificate:   "abc",
733
+						Key:           "abc",
734
+						CACertificate: "abc",
735
+					},
736
+				},
737
+			},
738
+			errorExpected: true,
739
+		},
740
+		{
741
+			name: "Edge termination mismatched key and cert",
742
+			route: &routeapi.Route{
743
+				Spec: routeapi.RouteSpec{
744
+					Host: "www.edge.mismatchdkeyandcert.test",
745
+					TLS: &routeapi.TLSConfig{
746
+						Termination:   routeapi.TLSTerminationEdge,
747
+						Certificate:   testCertificate,
748
+						Key:           testExpiredCertPrivateKey,
749
+						CACertificate: testCACertificate,
750
+					},
751
+				},
752
+			},
753
+			errorExpected: true,
754
+		},
755
+		{
756
+			name: "Edge termination expired cert",
757
+			route: &routeapi.Route{
758
+				Spec: routeapi.RouteSpec{
759
+					Host: "www.edge.expiredcert.test",
760
+					TLS: &routeapi.TLSConfig{
761
+						Termination:   routeapi.TLSTerminationEdge,
762
+						Certificate:   testExpiredCAUnknownCertificate,
763
+						Key:           testExpiredCertPrivateKey,
764
+						CACertificate: testCACertificate,
765
+					},
766
+				},
767
+			},
768
+			errorExpected: true,
769
+		},
770
+		{
771
+			name: "Edge termination expired cert key mismatch",
772
+			route: &routeapi.Route{
773
+				Spec: routeapi.RouteSpec{
774
+					Host: "www.edge.expiredcertkeymismatch.test",
775
+					TLS: &routeapi.TLSConfig{
776
+						Termination:   routeapi.TLSTerminationEdge,
777
+						Certificate:   testExpiredCAUnknownCertificate,
778
+						Key:           testPrivateKey,
779
+						CACertificate: testCACertificate,
780
+					},
781
+				},
782
+			},
783
+			errorExpected: true,
784
+		},
785
+		{
786
+			name: "Edge termination OK without certs",
787
+			route: &routeapi.Route{
788
+				Spec: routeapi.RouteSpec{
789
+					Host: "www.edge.nocerts.test",
790
+					TLS: &routeapi.TLSConfig{
791
+						Termination: routeapi.TLSTerminationEdge,
792
+					},
793
+				},
794
+			},
795
+			errorExpected: false,
796
+		},
797
+		{
798
+			name: "Edge termination, bad dest cert",
799
+			route: &routeapi.Route{
800
+				Spec: routeapi.RouteSpec{
801
+					Host: "www.edge.baddestcert.test",
802
+					TLS: &routeapi.TLSConfig{
803
+						Termination:              routeapi.TLSTerminationEdge,
804
+						DestinationCACertificate: "abc",
805
+					},
806
+				},
807
+			},
808
+			errorExpected: true,
809
+		},
810
+		{
811
+			name: "Passthrough termination, bad cert",
812
+			route: &routeapi.Route{
813
+				Spec: routeapi.RouteSpec{
814
+					Host: "www.passthrough.badcert.test",
815
+					TLS:  &routeapi.TLSConfig{Termination: routeapi.TLSTerminationPassthrough, Certificate: "test"},
816
+				},
817
+			},
818
+			errorExpected: true,
819
+		},
820
+		{
821
+			name: "Passthrough termination, bad key",
822
+			route: &routeapi.Route{
823
+				Spec: routeapi.RouteSpec{
824
+					Host: "www.passthrough.badkey.test",
825
+					TLS:  &routeapi.TLSConfig{Termination: routeapi.TLSTerminationPassthrough, Key: "test"},
826
+				},
827
+			},
828
+			errorExpected: true,
829
+		},
830
+		{
831
+			name: "Passthrough termination, bad ca cert",
832
+			route: &routeapi.Route{
833
+				Spec: routeapi.RouteSpec{
834
+					Host: "www.passthrough.badcacert.test",
835
+					TLS:  &routeapi.TLSConfig{Termination: routeapi.TLSTerminationPassthrough, CACertificate: "test"},
836
+				},
837
+			},
838
+			errorExpected: true,
839
+		},
840
+		{
841
+			name: "Passthrough termination, bad dest ca cert",
842
+			route: &routeapi.Route{
843
+				Spec: routeapi.RouteSpec{
844
+					Host: "www.passthrough.baddestcacert.test",
845
+					TLS:  &routeapi.TLSConfig{Termination: routeapi.TLSTerminationPassthrough, DestinationCACertificate: "test"},
846
+				},
847
+			},
848
+			errorExpected: true,
849
+		},
850
+		{
851
+			name: "Invalid termination type",
852
+			route: &routeapi.Route{
853
+				Spec: routeapi.RouteSpec{
854
+					TLS: &routeapi.TLSConfig{
855
+						Termination: "invalid",
856
+					},
857
+				},
858
+			},
859
+			errorExpected: false,
860
+		},
861
+		{
862
+			name: "Double escaped newlines",
863
+			route: &routeapi.Route{
864
+				Spec: routeapi.RouteSpec{
865
+					Host: "www.reencrypt.doubleescapednewlines.test",
866
+					TLS: &routeapi.TLSConfig{
867
+						Termination:              routeapi.TLSTerminationReencrypt,
868
+						Certificate:              "d\\nef",
869
+						Key:                      "g\\nhi",
870
+						CACertificate:            "j\\nkl",
871
+						DestinationCACertificate: "j\\nkl",
872
+					},
873
+				},
874
+			},
875
+			errorExpected: true,
876
+		},
877
+	}
878
+
879
+	for _, tc := range tests {
880
+		err := plugin.HandleRoute(watch.Added, tc.route)
881
+		if tc.errorExpected {
882
+			if err == nil {
883
+				t.Fatalf("test case %s: expected an error, got none", tc.name)
884
+			}
885
+		} else {
886
+			if err != nil {
887
+				t.Fatalf("test case %s: expected no errors, got %v", tc.name, err)
888
+			}
889
+		}
890
+	}
891
+}
892
+
539 893
 func TestNamespaceScopingFromEmpty(t *testing.T) {
540 894
 	router := newTestRouter(make(map[string]ServiceUnit))
541 895
 	templatePlugin := newDefaultTemplatePlugin(router, true)
... ...
@@ -306,7 +306,7 @@ func (r *templateRouter) reloadRouter() error {
306 306
 	cmd := exec.Command(r.reloadScriptPath)
307 307
 	out, err := cmd.CombinedOutput()
308 308
 	if err != nil {
309
-		return fmt.Errorf("error reloading router: %v\n%s", err, out)
309
+		return fmt.Errorf("error reloading router: %v\n%s", err, string(out))
310 310
 	}
311 311
 	glog.Infof("Router reloaded:\n%s", out)
312 312
 	return nil
... ...
@@ -319,7 +319,7 @@ func TestAddRoute(t *testing.T) {
319 319
 		saCfg, ok := su.ServiceAliasConfigs[routeKey]
320 320
 
321 321
 		if !ok {
322
-			t.Errorf("Unable to find created serivce alias config for route %s", routeKey)
322
+			t.Errorf("Unable to find created service alias config for route %s", routeKey)
323 323
 		} else {
324 324
 			if saCfg.Host != route.Spec.Host || saCfg.Path != route.Spec.Path || !compareTLS(route, saCfg, t) {
325 325
 				t.Errorf("Route %v did not match serivce alias config %v", route, saCfg)