diff --git a/core/citrus-base/src/main/java/org/citrusframework/annotations/CitrusAnnotations.java b/core/citrus-base/src/main/java/org/citrusframework/annotations/CitrusAnnotations.java index 6275ac412c..43fbc0649d 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/annotations/CitrusAnnotations.java +++ b/core/citrus-base/src/main/java/org/citrusframework/annotations/CitrusAnnotations.java @@ -26,6 +26,7 @@ import org.citrusframework.TestActionRunner; import org.citrusframework.TestCaseRunner; import org.citrusframework.common.InitializingPhase; +import org.citrusframework.common.Named; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.BindToRegistry; @@ -249,6 +250,11 @@ public static void parseConfiguration(Object configuration, CitrusContext citrus try { String name = ReferenceRegistry.getName(m.getAnnotation(BindToRegistry.class), m.getName()); Object component = m.invoke(configuration); + + if (component instanceof Named named) { + named.setName(name); + } + citrusContext.getReferenceResolver().bind(name, component); initializeComponent(name, component, citrusContext); @@ -270,6 +276,11 @@ public static void parseConfiguration(Object configuration, CitrusContext citrus try { String name = ReferenceRegistry.getName(f.getAnnotation(BindToRegistry.class), f.getName()); Object component = f.get(configuration); + + if (component instanceof Named named) { + named.setName(name); + } + citrusContext.getReferenceResolver().bind(name, component); initializeComponent(name, component, citrusContext); diff --git a/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncEndpointConfiguration.java b/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncEndpointConfiguration.java index fdffacad0e..a4927bf846 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncEndpointConfiguration.java +++ b/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncEndpointConfiguration.java @@ -33,7 +33,7 @@ public MessageCorrelator getCorrelator() { /** * Gets the pollingInterval. - * @return the pollingInterval the pollingInterval to get. + * @return the pollingInterval to get. */ public long getPollingInterval() { return pollingInterval; diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/BasicAuthClientHttpRequestFactory.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/BasicAuthClientHttpRequestFactory.java index 05a773922d..6127637046 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/BasicAuthClientHttpRequestFactory.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/BasicAuthClientHttpRequestFactory.java @@ -21,10 +21,10 @@ import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; -import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.auth.BasicAuthCache; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.impl.auth.BasicScheme; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.HttpHost; @@ -45,7 +45,7 @@ public class BasicAuthClientHttpRequestFactory implements FactoryBean, InitializingPhase { /** The target request factory */ - private HttpClient httpClient; + private HttpClientBuilder httpClient; /** User credentials for basic authentication */ private Credentials credentials; @@ -59,7 +59,7 @@ public class BasicAuthClientHttpRequestFactory implements FactoryBean { /** Endpoint target */ - private HttpClient endpoint = new HttpClient(); + private final HttpClient endpoint = new HttpClient(); @Override protected HttpClient getEndpoint() { @@ -234,4 +236,25 @@ public HttpClientBuilder timeout(long timeout) { endpoint.getEndpointConfiguration().setTimeout(timeout); return this; } + + /** + * Sets the user authentication. + * @param auth + * @return + */ + public HttpClientBuilder authentication(HttpAuthentication auth) { + endpoint.getEndpointConfiguration().setRequestFactory( + auth.getRequestFactory(endpoint.getEndpointConfiguration().getRequestUrl(), endpoint)); + return this; + } + + /** + * Enable secured connection on the client using provided SSL connection. + * @return + */ + public HttpClientBuilder secured(HttpSecureConnection conn) { + endpoint.getEndpointConfiguration().getHttpClient() + .setConnectionManager(conn.getClientConnectionManager()); + return this; + } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/HttpEndpointConfiguration.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/HttpEndpointConfiguration.java index 834bd28975..c8f95e6381 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/HttpEndpointConfiguration.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/HttpEndpointConfiguration.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.citrusframework.endpoint.AbstractPollableEndpointConfiguration; import org.citrusframework.endpoint.resolver.DynamicEndpointUriResolver; import org.citrusframework.endpoint.resolver.EndpointUriResolver; @@ -62,6 +63,9 @@ public class HttpEndpointConfiguration extends AbstractPollableEndpointConfigura /** The rest template */ private RestTemplate restTemplate; + /** Http client builder */ + private HttpClientBuilder httpClient; + /** Request factory */ private ClientHttpRequestFactory requestFactory; @@ -75,7 +79,7 @@ public class HttpEndpointConfiguration extends AbstractPollableEndpointConfigura private HttpMessageConverter messageConverter = new HttpMessageConverter(); /** Endpoint clientInterceptors */ - private List clientInterceptors; + private List clientInterceptors = new ArrayList<>(); /** Should http errors be handled within endpoint consumer or simply throw exception */ private ErrorHandlingStrategy errorHandlingStrategy = ErrorHandlingStrategy.PROPAGATE; @@ -110,9 +114,7 @@ public class HttpEndpointConfiguration extends AbstractPollableEndpointConfigura * Default constructor initializes with default logging interceptor. */ public HttpEndpointConfiguration() { - List interceptors = new ArrayList<>(); - interceptors.add(new LoggingClientInterceptor()); - setClientInterceptors(interceptors); + clientInterceptors.add(new LoggingClientInterceptor()); } /** @@ -228,6 +230,7 @@ public String getContentType() { public RestTemplate getRestTemplate() { if (restTemplate == null) { restTemplate = new RestTemplate(); + restTemplate.setInterceptors(clientInterceptors); } restTemplate.setRequestFactory(getRequestFactory()); @@ -298,7 +301,7 @@ public MessageCorrelator getCorrelator() { */ public ClientHttpRequestFactory getRequestFactory() { if (requestFactory == null) { - requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory = new HttpComponentsClientHttpRequestFactory(getHttpClient().build()); } return requestFactory; @@ -312,6 +315,18 @@ public void setRequestFactory(ClientHttpRequestFactory requestFactory) { this.requestFactory = requestFactory; } + public HttpClientBuilder getHttpClient() { + if (httpClient == null) { + httpClient = HttpClientBuilder.create().useSystemProperties(); + } + + return httpClient; + } + + public void setHttpClient(HttpClientBuilder httpClient) { + this.httpClient = httpClient; + } + /** * Gets the message converter. * @return diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpClientConfig.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpClientConfig.java index f2be3d60e4..f885612d36 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpClientConfig.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpClientConfig.java @@ -141,4 +141,16 @@ * @return */ String actor() default ""; + + /** + * User authentication. + * @return + */ + String authentication() default ""; + + /** + * Secured connection. + * @return + */ + String secured() default ""; } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpClientConfigParser.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpClientConfigParser.java index e25e3f971e..5826a7857f 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpClientConfigParser.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpClientConfigParser.java @@ -26,6 +26,8 @@ import org.citrusframework.http.client.HttpClient; import org.citrusframework.http.client.HttpClientBuilder; import org.citrusframework.http.message.HttpMessageConverter; +import org.citrusframework.http.security.HttpAuthentication; +import org.citrusframework.http.security.HttpSecureConnection; import org.citrusframework.message.MessageCorrelator; import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.util.StringUtils; @@ -112,6 +114,14 @@ public HttpClient parse(HttpClientConfig annotation, ReferenceResolver reference builder.actor(referenceResolver.resolve(annotation.actor(), TestActor.class)); } + if (StringUtils.hasText(annotation.authentication())) { + builder.authentication(referenceResolver.resolve(annotation.authentication(), HttpAuthentication.class)); + } + + if (StringUtils.hasText(annotation.secured())) { + builder.secured(referenceResolver.resolve(annotation.secured(), HttpSecureConnection.class)); + } + return builder.initialize().build(); } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpServerConfig.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpServerConfig.java index 679cca2772..ccc21bf742 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpServerConfig.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpServerConfig.java @@ -182,4 +182,28 @@ * @return */ String actor() default ""; + + /** + * User authentication. + * @return + */ + String authentication() default ""; + + /** + * Resource path that is secured with user authentication. + * @return + */ + String securedPath() default "/*"; + + /** + * Secured connection. + * @return + */ + String secured() default ""; + + /** + * Secured server port. + * @return + */ + int securePort() default 8443; } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpServerConfigParser.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpServerConfigParser.java index 57cefe99ac..01c21554a9 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpServerConfigParser.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/annotation/HttpServerConfigParser.java @@ -29,6 +29,8 @@ import org.citrusframework.endpoint.EndpointAdapter; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.http.message.HttpMessageConverter; +import org.citrusframework.http.security.HttpAuthentication; +import org.citrusframework.http.security.HttpSecureConnection; import org.citrusframework.http.server.HttpServer; import org.citrusframework.http.server.HttpServerBuilder; import org.citrusframework.spi.ReferenceResolver; @@ -138,6 +140,14 @@ public HttpServer parse(HttpServerConfig annotation, ReferenceResolver reference builder.defaultStatus(annotation.defaultStatus()); builder.responseCacheSize(annotation.responseCacheSize()); + if (StringUtils.hasText(annotation.authentication())) { + builder.authentication(annotation.securedPath(), referenceResolver.resolve(annotation.authentication(), HttpAuthentication.class)); + } + + if (StringUtils.hasText(annotation.secured())) { + builder.secured(annotation.securePort(), referenceResolver.resolve(annotation.secured(), HttpSecureConnection.class)); + } + return builder.initialize().build(); } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/BasicAuthentication.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/BasicAuthentication.java new file mode 100644 index 0000000000..18ba38bdf9 --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/BasicAuthentication.java @@ -0,0 +1,111 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.http.security; + +import java.net.URL; +import java.util.Collections; + +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.client.BasicAuthClientHttpRequestFactory; +import org.citrusframework.http.client.HttpClient; +import org.eclipse.jetty.security.SecurityHandler; +import org.eclipse.jetty.security.authentication.BasicAuthenticator; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.util.StringUtils; + +/** + * Basic authentication implementation able to create a proper request factory with basic auth client credentials. + * Trying to read hostname and port from given request URL on the Http client in order to set a proper auth scope. + */ +public class BasicAuthentication implements HttpAuthentication { + + private final String username; + private final String password; + + private String realm = ""; + + private String[] userRoles = new String[] { "citrus" }; + + public BasicAuthentication(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public SecurityHandler getSecurityHandler(String resourcePath) { + try { + SecurityHandlerFactory securityHandlerFactory = new SecurityHandlerFactory(); + securityHandlerFactory.setUsers(Collections.singletonList(new User(username, password, userRoles))); + securityHandlerFactory.setConstraints(Collections.singletonMap(resourcePath, new BasicAuthConstraint(userRoles))); + + securityHandlerFactory.setAuthenticator(new BasicAuthenticator()); + securityHandlerFactory.setRealm(realm); + + securityHandlerFactory.initialize(); + + return securityHandlerFactory.getObject(); + } catch (Exception e) { + throw new CitrusRuntimeException(e); + } + } + + @Override + public ClientHttpRequestFactory getRequestFactory(String requestUrl, HttpClient httpClient) { + try { + BasicAuthClientHttpRequestFactory requestFactory = new BasicAuthClientHttpRequestFactory(); + + if (httpClient != null) { + requestFactory.setHttpClient(httpClient.getEndpointConfiguration().getHttpClient()); + } + + URL url = null; + if (StringUtils.hasText(requestUrl)) { + url = new URL(requestUrl); + } else if (httpClient != null && StringUtils.hasText(httpClient.getEndpointConfiguration().getRequestUrl())) { + url = new URL(httpClient.getEndpointConfiguration().getRequestUrl()); + } + + if (url != null) { + AuthScope authScope = new AuthScope(url.getProtocol(), url.getHost(), url.getPort(), realm, "basic"); + requestFactory.setAuthScope(authScope); + } + + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password.toCharArray()); + requestFactory.setCredentials(credentials); + + requestFactory.initialize(); + return requestFactory.getObject(); + } catch (Exception e) { + throw new CitrusRuntimeException("Failed to configure basic auth on Http client", e); + } + } + + public BasicAuthentication realm(String realm) { + this.realm = realm; + return this; + } + + public BasicAuthentication userRoles(String... roles) { + this.userRoles = roles; + return this; + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpAuthentication.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpAuthentication.java new file mode 100644 index 0000000000..98c6cd56b2 --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpAuthentication.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.http.security; + +import org.citrusframework.http.client.HttpClient; +import org.eclipse.jetty.security.SecurityHandler; +import org.springframework.http.client.ClientHttpRequestFactory; + +public interface HttpAuthentication { + + /** + * Security handler able to set up server user authentication on given resource path. + * @param resourcePath + * @return + */ + SecurityHandler getSecurityHandler(String resourcePath); + + /** + * Creates client request factory that uses the given authentication method. + * @param requestUrl + * @param httpClient + * @return + */ + ClientHttpRequestFactory getRequestFactory(String requestUrl, HttpClient httpClient); + + static BasicAuthentication basic(String username, String password) { + return new BasicAuthentication(username, password); + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpClientConnectionManagerFactory.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpClientConnectionManagerFactory.java new file mode 100644 index 0000000000..707d4d71cb --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpClientConnectionManagerFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.http.security; + +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.springframework.beans.factory.FactoryBean; + +/** + * Spring bean factory for a secure SSL connection manager. + * Factory may be used in Spring bean configuration to set connection manager on a Http client. + */ +public class HttpClientConnectionManagerFactory implements FactoryBean { + + private final HttpSecureConnection secureConnection; + + public HttpClientConnectionManagerFactory(HttpSecureConnection secureConnection) { + this.secureConnection = secureConnection; + } + + @Override + public HttpClientConnectionManager getObject() throws Exception { + return secureConnection.getClientConnectionManager(); + } + + @Override + public Class getObjectType() { + return HttpClientConnectionManager.class; + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpClientRequestFactoryBean.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpClientRequestFactoryBean.java new file mode 100644 index 0000000000..d225ec0bdf --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpClientRequestFactoryBean.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.http.security; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; + +public class HttpClientRequestFactoryBean implements FactoryBean { + + private final HttpAuthentication auth; + private String requestUrl; + + public HttpClientRequestFactoryBean(HttpAuthentication auth) { + this.auth = auth; + } + + @Override + public ClientHttpRequestFactory getObject() throws Exception { + return auth.getRequestFactory(requestUrl, null); + } + + @Override + public Class getObjectType() { + return HttpComponentsClientHttpRequestFactory.class; + } + + public void setRequestUrl(String requestUrl) { + this.requestUrl = requestUrl; + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpSecureConnection.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpSecureConnection.java new file mode 100644 index 0000000000..fd89b453ba --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpSecureConnection.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.http.security; + +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.eclipse.jetty.server.ServerConnector; + +/** + * Secure Http connection able to create proper connection manager for clients. + */ +public interface HttpSecureConnection { + + /** + * Create server connector representing the secure connection. + * @param securePort + * @return + */ + ServerConnector getServerConnector(int securePort); + + /** + * Create proper secure connection manager to be used on custom Http clients. + * @return + */ + HttpClientConnectionManager getClientConnectionManager(); + + static SSLConnection ssl() { + return new SSLConnection(); + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpSecurityHandlerFactoryBean.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpSecurityHandlerFactoryBean.java new file mode 100644 index 0000000000..70dca39e78 --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpSecurityHandlerFactoryBean.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.http.security; + +import org.eclipse.jetty.security.SecurityHandler; +import org.springframework.beans.factory.FactoryBean; + +public class HttpSecurityHandlerFactoryBean implements FactoryBean { + + private final HttpAuthentication auth; + private String resourcePath = "/*"; + + public HttpSecurityHandlerFactoryBean(HttpAuthentication auth) { + this.auth = auth; + } + + @Override + public SecurityHandler getObject() throws Exception { + return auth.getSecurityHandler(resourcePath); + } + + @Override + public Class getObjectType() { + return SecurityHandler.class; + } + + public void setResourcePath(String resourcePath) { + this.resourcePath = resourcePath; + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpServerConnectorFactory.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpServerConnectorFactory.java new file mode 100644 index 0000000000..8ba3ce4bf2 --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/HttpServerConnectorFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.http.security; + +import org.eclipse.jetty.server.ServerConnector; +import org.springframework.beans.factory.FactoryBean; + +/** + * Spring factory bean for server connector. + */ +public class HttpServerConnectorFactory implements FactoryBean { + + private final HttpSecureConnection secureConnection; + private int securePort = 8443; + + public HttpServerConnectorFactory(HttpSecureConnection secureConnection) { + this.secureConnection = secureConnection; + } + + @Override + public ServerConnector getObject() throws Exception { + return secureConnection.getServerConnector(securePort); + } + + @Override + public Class getObjectType() { + return ServerConnector.class; + } + + public void setSecurePort(int securePort) { + this.securePort = securePort; + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/SSLConnection.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/SSLConnection.java new file mode 100644 index 0000000000..e56d0e0542 --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/SSLConnection.java @@ -0,0 +1,176 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.http.security; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Collections; +import javax.net.ssl.HostnameVerifier; + +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.TrustAllStrategy; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.ssl.SSLContexts; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.springframework.beans.factory.BeanCreationException; + +/** + * SSL secure connection to set up a proper SSL context and SSL connection socket factory. + * Optionally uses provided keystore and truststore. If not set uses trust all strategy. + */ +public class SSLConnection implements HttpSecureConnection { + + private Resource keyStore; + private String keyStorePassword; + + private Resource trustStore; + + private String trustStorePassword; + + private HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE; + + public SSLConnection() { + } + + public SSLConnection(Resource keyStore, String password) { + this.keyStore = keyStore; + this.keyStorePassword = password; + } + + public SSLConnection(Resource keyStore, String ksPassword, Resource trustStore, String tsPassword) { + this.keyStore = keyStore; + this.keyStorePassword = ksPassword; + this.trustStore = trustStore; + this.trustStorePassword = tsPassword; + } + + @Override + public ServerConnector getServerConnector(int securePort) { + ServerConnector connector = new ServerConnector(new Server(), + new SslConnectionFactory(sslContextFactory(), HttpVersion.HTTP_1_1.asString()), + new HttpConnectionFactory(httpConfiguration(securePort))); + connector.setPort(securePort); + + return connector; + } + + @Override + public HttpClientConnectionManager getClientConnectionManager() { + try { + SSLContextBuilder sslContext; + + if (trustStore != null) { + sslContext = SSLContexts.custom() + .loadTrustMaterial(trustStore.getFile(), trustStorePassword.toCharArray(), + new TrustSelfSignedStrategy()); + } else { + sslContext = SSLContexts.custom() + .loadTrustMaterial(TrustAllStrategy.INSTANCE); + } + + if (keyStore != null) { + sslContext.loadKeyMaterial(KeyStore.getInstance(keyStore.getFile(), keyStorePassword.toCharArray()), + keyStorePassword.toCharArray()); + } + + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( + sslContext.build(), hostnameVerifier); + + return PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslSocketFactory) + .build(); + } catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException | + KeyManagementException | UnrecoverableKeyException e) { + throw new BeanCreationException("Failed to create http client for ssl connection", e); + } + } + + public SSLConnection trustStore(String trustStore, String password) { + return trustStore(Resources.create(trustStore), password); + } + + public SSLConnection trustStore(Resource trustStore, String password) { + this.trustStore = trustStore; + this.trustStorePassword = password; + return this; + } + + public SSLConnection keyStore(String keyStore, String password) { + return keyStore(Resources.create(keyStore), password); + } + + public SSLConnection keyStore(Resource keyStore, String password) { + this.keyStore = keyStore; + this.keyStorePassword = password; + return this; + } + + public SSLConnection hostnameVerifier(HostnameVerifier verifier) { + this.hostnameVerifier = verifier; + return this; + } + + private SslContextFactory.Server sslContextFactory() { + SslContextFactory.Server contextFactory = new SslContextFactory.Server(); + + if (trustStore != null) { + contextFactory.setTrustStorePath(trustStore.getFile().getPath()); + contextFactory.setTrustStorePassword(trustStorePassword); + } else { + contextFactory.setTrustAll(true); + } + + if (keyStore != null) { + contextFactory.setKeyStorePath(keyStore.getFile().getPath()); + contextFactory.setKeyStorePassword(keyStorePassword); + } + + return contextFactory; + } + + private HttpConfiguration httpConfiguration(int securePort) { + HttpConfiguration parent = new HttpConfiguration(); + parent.setSecureScheme("https"); + parent.setSecurePort(securePort); + HttpConfiguration configuration = new HttpConfiguration(parent); + SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer(); + secureRequestCustomizer.setSniHostCheck(false); + configuration.setCustomizers(Collections.singletonList(secureRequestCustomizer)); + return configuration; + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/SecurityHandlerFactory.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/SecurityHandlerFactory.java index 17f2ce957d..25deb223a1 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/SecurityHandlerFactory.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/security/SecurityHandlerFactory.java @@ -16,9 +16,6 @@ package org.citrusframework.http.security; -import javax.security.auth.Subject; -import java.io.IOException; -import java.security.Principal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -26,7 +23,6 @@ import java.util.Map.Entry; import org.citrusframework.common.InitializingPhase; -import org.eclipse.jetty.security.AbstractLoginService; import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintSecurityHandler; @@ -49,14 +45,14 @@ */ public class SecurityHandlerFactory implements FactoryBean, InitializingPhase { - /** User credentials known to login service */ + /** User credentials known to the login service */ private List users = new ArrayList<>(); /** Realm name for this security handler */ private String realm = "realm"; /** List of constraints with mapping path as key */ - private Map constraints = new HashMap(); + private Map constraints = new HashMap<>(); /** User login service consolidated for user authentication */ private LoginService loginService; diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/AbstractHttpServerBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/AbstractHttpServerBuilder.java index 2e84011a66..751536b29e 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/AbstractHttpServerBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/AbstractHttpServerBuilder.java @@ -19,11 +19,13 @@ package org.citrusframework.http.server; -import jakarta.servlet.Filter; import java.util.List; import java.util.Map; +import jakarta.servlet.Filter; import org.citrusframework.http.message.HttpMessageConverter; +import org.citrusframework.http.security.HttpAuthentication; +import org.citrusframework.http.security.HttpSecureConnection; import org.citrusframework.server.AbstractServerBuilder; import org.eclipse.jetty.security.SecurityHandler; import org.eclipse.jetty.server.Connector; @@ -40,6 +42,8 @@ public class AbstractHttpServerBuilder ---- -Citrus will add this header to the Http requests and the server will read the *Authorization* username and password. For -more convenient base64 encoding you can also use a Citrus function, see link:#functions-encode-base64[functions-encode-base64] +Citrus will add this header to the Http requests and the server will read the *Authorization* username and password. +For more convenient base64 encoding you can also use a Citrus function `citrus:encodeBase64(username:password)`. +See link:#functions-encode-base64[functions-encode-base64] for more details. -Now there is a more comfortable way to set the basic authentication header in all the Citrus requests. As Citrus uses -Spring's REST support with the RestTemplate and ClientHttpRequestFactory the basic authentication is already covered there -in a more generic way. You simply have to configure the basic authentication credentials on the RestTemplate's ClientHttpRequestFactory. +Instead of adding the Http *Authorization* header manually you can also enable the basic authentication on the Http client endpoint. +Simply configure the basic authentication credentials on the Citrus Http client component. Just see the following example and learn how to do that. +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public HttpClient httpClient() { + return new HttpClientBuilder() + .requestUrl("http://localhost:8080/test") + .authentication(HttpAuthentication.basic("username", "password")) + .build(); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + + + +---- + +In case you need a more powerful configuration of the basic authentication you can also directly use the request factory as a configuration item for basic authentication. + .Java [source,java,indent=0,role="primary"] ---- @@ -1250,7 +1282,7 @@ public BasicAuthClientHttpRequestFactory basicAuthFactory() { AuthScope scope = new AuthScope("localhost", "8080", "", "basic"); factory.setAuthScope(scope); - UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("someUsername", "somePassword"); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("username", "password"); factory.setCredentials(credentials); return factory; @@ -1274,21 +1306,21 @@ public BasicAuthClientHttpRequestFactory basicAuthFactory() { - - + + ---- -The advantages of this method is obvious. Now all sending test actions that reference the client component will automatically -add the basic authentication header. +The advantage of setting the basic authentication on the Http client is obvious. +All test actions that reference the client component will automatically use the basic authentication. The above configuration results in Http client requests with authentication headers properly set for basic authentication. The client request factory takes care of adding the proper basic authentication header to each request that is sent with this Citrus message sender. Citrus uses preemptive authentication. The message sender only sends a single request to the server with all authentication information set in the message header. The request which determines the authentication scheme on the -server is skipped. This is why you have to add some auth scope in the client request factory so Citrus can setup an authentication +server is skipped. This is why you have to add some auth scope in the client request factory so Citrus can set up an authentication cache within the Http context in order to have preemptive authentication. As a result of the basic auth client request factory the following example request that is created by the Citrus Http client @@ -1313,7 +1345,41 @@ Content-Length: 175 [[http-server-basic-authentication]] == Http server basic authentication -Citrus as a server can also set basic authentication so clients need to authenticate properly when accessing server resources. +The Citrus Http server is able to configure basic authentication so clients need to authenticate properly when accessing server resources. + +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public HttpServer httpServer() { + return new HttpServerBuilder() + .port(8080) + .autoStart(true) + .authentication("/foo/*", HttpAuthentication.basic("username", "password")) + .build(); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + + + + +---- + +In case you need a more powerful server configuration you can also set the security handler directly. .Java [source,java,indent=0,role="primary"] @@ -1338,7 +1404,7 @@ public SecurityHandlerFactory securityHandler() { factory.setUsers(Collections.singletonList(user)); factory.setConstraints(Collections.singletonMap("/foo/*", - new BasicAuthConstraint("CitrusRole"))); + new BasicAuthConstraint("CitrusRole"))); return factory; } @@ -1385,6 +1451,121 @@ Just have a look at the code base and inspect the settings and properties offere TIP: This mechanism is not restricted to basic authentication only. With other settings you can also set up digest or form-based authentication constraints very easy. +[[http-client-ssl]] +== Http client SSL + +You can use SSL when establishing a client connection with the server. +The Citrus Http client supports a secure connection with these settings. + +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public HttpClient httpClient() { + return new HttpClientBuilder() + .requestUrl("https://localhost:8080/test") + .secured(HttpSecureConnection.ssl()) + .build(); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + +---- + +The example Http client uses SSL secure connection. +By default, the client will accept any server certificates and hostnames often referred to as trust-all-strategy. + +You may want to also use a proper trust store to explicitly accept server certificates. +The client by default allows self-signed certificates. + +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public HttpClient sslHttpClient() { + return new HttpClientBuilder() + .requestUrl("https://localhost:8080/test") + .secured(HttpSecureConnection.ssl() + .trustStore("path/to/server.jks", "trustStorePassword")) + .build(); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + + + +---- + +You need to provide a key store file (.jks) and a password to access the certificates located in the store. +The client now will do proper SSL handshake with the server in order to use secure connections. + +[[http-server-ssl]] +== Http server SSL + +The Citrus Http server is able to configure a SSL connector so clients need to use secure connections to access the server resources. +The connector + +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public HttpServer sslHttpServer() { + return new HttpServerBuilder() + .port(8080) + .autoStart(true) + .secured(8443, HttpSecureConnection.ssl() + .keyStore("path/to/server.jks", "keyStorePassword")) + .build(); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + + + + +---- + +The server now uses a `securePort` and a server certificate. +In most cases the certificate in a test environment is self-signed so please make sure to allow self-signed certificates in your clients. + [[http-cookies]] == Http cookies