From 864036674ec101c81701bbd8fbb7f96ad25dd3be Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Tue, 19 Jul 2022 23:15:57 -0500 Subject: [PATCH] NIFI-10244 Added nifi-web-client-api and implementation - Added nifi-web-client implementation based on OkHttp - Added WebClientServiceProvider Controller Service interface and implementation - Corrected comments and added unmodifiableMap wrapper - Added getHeaderNames() and corrected ProxyContext comments This closes #6268 Signed-off-by: Paul Grey --- nifi-assembly/pom.xml | 6 + nifi-commons/nifi-web-client-api/pom.xml | 25 + .../web/client/api/HttpEntityHeaders.java | 49 ++ .../web/client/api/HttpRequestBodySpec.java | 34 ++ .../client/api/HttpRequestHeadersSpec.java | 38 ++ .../web/client/api/HttpRequestMethod.java | 29 ++ .../web/client/api/HttpRequestUriSpec.java | 32 ++ .../web/client/api/HttpResponseEntity.java | 46 ++ .../web/client/api/HttpResponseStatus.java | 58 +++ .../nifi/web/client/api/HttpUriBuilder.java | 80 ++++ .../client/api/StandardHttpRequestMethod.java | 37 ++ .../nifi/web/client/api/WebClientService.java | 65 +++ .../client/api/WebClientServiceException.java | 45 ++ nifi-commons/nifi-web-client/pom.xml | 42 ++ .../web/client/BasicProxyAuthenticator.java | 43 ++ .../web/client/InputStreamRequestBody.java | 56 +++ .../web/client/StandardHttpEntityHeaders.java | 55 +++ .../client/StandardHttpResponseEntity.java | 64 +++ .../web/client/StandardHttpUriBuilder.java | 80 ++++ .../web/client/StandardWebClientService.java | 303 ++++++++++++ .../nifi/web/client/proxy/ProxyContext.java | 46 ++ .../web/client/redirect/RedirectHandling.java | 28 ++ .../client/ssl/SSLSocketFactoryProvider.java | 32 ++ .../ssl/StandardSSLSocketFactoryProvider.java | 70 +++ .../nifi/web/client/ssl/TlsContext.java | 47 ++ .../client/StandardHttpUriBuilderTest.java | 134 ++++++ .../client/StandardWebClientServiceTest.java | 434 ++++++++++++++++++ nifi-commons/pom.xml | 2 + .../nifi-standard-services-api-nar/pom.xml | 6 + .../nifi-web-client-provider-api/pom.xml | 35 ++ .../api/WebClientServiceProvider.java | 40 ++ .../pom.xml | 38 ++ .../nifi-web-client-provider-service/pom.xml | 72 +++ .../provider/service/KeyManagerProvider.java | 35 ++ .../service/StandardKeyManagerProvider.java | 109 +++++ .../StandardWebClientServiceProvider.java | 206 +++++++++ ...g.apache.nifi.controller.ControllerService | 15 + .../StandardKeyManagerProviderTest.java | 76 +++ .../StandardWebClientServiceProviderTest.java | 245 ++++++++++ .../nifi-web-client-provider-bundle/pom.xml | 30 ++ .../nifi-standard-services/pom.xml | 1 + 41 files changed, 2888 insertions(+) create mode 100644 nifi-commons/nifi-web-client-api/pom.xml create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java create mode 100644 nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java create mode 100644 nifi-commons/nifi-web-client/pom.xml create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java create mode 100644 nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java create mode 100644 nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java create mode 100644 nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java create mode 100644 nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml index caf87d0a18..c50e2ca7fe 100644 --- a/nifi-assembly/pom.xml +++ b/nifi-assembly/pom.xml @@ -844,6 +844,12 @@ language governing permissions and limitations under the License. --> 1.18.0-SNAPSHOT nar + + org.apache.nifi + nifi-web-client-provider-service-nar + 1.18.0-SNAPSHOT + nar + diff --git a/nifi-commons/nifi-web-client-api/pom.xml b/nifi-commons/nifi-web-client-api/pom.xml new file mode 100644 index 0000000000..b1d67b506c --- /dev/null +++ b/nifi-commons/nifi-web-client-api/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-commons + 1.18.0-SNAPSHOT + + nifi-web-client-api + Abstracts standard HTTP client operations without depending on a specific HTTP library + diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java new file mode 100644 index 0000000000..a402de153e --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java @@ -0,0 +1,49 @@ +/* + * 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.apache.nifi.web.client.api; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * HTTP Entity Headers supporting retrieval of single or multiple header values + */ +public interface HttpEntityHeaders { + /** + * Get First Header using specified Header Name + * + * @param headerName Header Name to be retrieved + * @return First Header Value or empty when not found + */ + Optional getFirstHeader(String headerName); + + /** + * Get Header Values using specified Header Name + * + * @param headerName Header Name to be retrieved + * @return List of Header Values or empty when not found + */ + List getHeader(String headerName); + + /** + * Get Header Names + * + * @return Collection of Header Names or empty when not found + */ + Collection getHeaderNames(); +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java new file mode 100644 index 0000000000..9ea80bab4d --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java @@ -0,0 +1,34 @@ +/* + * 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.apache.nifi.web.client.api; + +import java.io.InputStream; +import java.util.OptionalLong; + +/** + * HTTP Request Body Specification builder + */ +public interface HttpRequestBodySpec extends HttpRequestHeadersSpec { + /** + * Set Request Body as stream + * + * @param inputStream Request Body stream is required + * @param contentLength Content Length or empty when not known + * @return HTTP Request Headers Specification builder + */ + HttpRequestHeadersSpec body(InputStream inputStream, OptionalLong contentLength); +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java new file mode 100644 index 0000000000..8ed965cff5 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java @@ -0,0 +1,38 @@ +/* + * 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.apache.nifi.web.client.api; + +/** + * HTTP Request Headers Specification builder + */ +public interface HttpRequestHeadersSpec { + /** + * Add HTTP Request Header using specified name and value + * + * @param headerName HTTP Header Name + * @param headerValue HTTP Header Value + * @return HTTP Request Body Specification builder + */ + HttpRequestBodySpec header(String headerName, String headerValue); + + /** + * Execute HTTP Request and retrieve HTTP Response + * + * @return HTTP Response Entity + */ + HttpResponseEntity retrieve(); +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java new file mode 100644 index 0000000000..80f94864d7 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java @@ -0,0 +1,29 @@ +/* + * 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.apache.nifi.web.client.api; + +/** + * HTTP Request Method + */ +public interface HttpRequestMethod { + /** + * Get HTTP Method + * + * @return HTTP Method + */ + String getMethod(); +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java new file mode 100644 index 0000000000..cad4cd74d3 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java @@ -0,0 +1,32 @@ +/* + * 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.apache.nifi.web.client.api; + +import java.net.URI; + +/** + * HTTP Request URI Specification builder + */ +public interface HttpRequestUriSpec { + /** + * Create HTTP Request Body builder using specified Request URI + * + * @param uri Request URI + * @return HTTP Request Body Specification builder + */ + HttpRequestBodySpec uri(URI uri); +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java new file mode 100644 index 0000000000..f0386aece1 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java @@ -0,0 +1,46 @@ +/* + * 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.apache.nifi.web.client.api; + +import java.io.Closeable; +import java.io.InputStream; + +/** + * HTTP Response Entity extends Closeable to handle closing Response Body + */ +public interface HttpResponseEntity extends Closeable { + /** + * Get HTTP Response Status Code + * + * @return HTTP Response Status Code + */ + int statusCode(); + + /** + * Get HTTP Response Headers + * + * @return HTTP Response Headers + */ + HttpEntityHeaders headers(); + + /** + * Get HTTP Response Body stream + * + * @return HTTP Response Body stream can be empty + */ + InputStream body(); +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java new file mode 100644 index 0000000000..915cf3cb63 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java @@ -0,0 +1,58 @@ +/* + * 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.apache.nifi.web.client.api; + +/** + * Enumeration of Standard HTTP Response Status Codes + */ +public enum HttpResponseStatus { + OK(200), + + CREATED(201), + + ACCEPTED(202), + + NO_CONTENT(204), + + MOVED_PERMANENTLY(301), + + BAD_REQUEST(400), + + UNAUTHORIZED(401), + + FORBIDDEN(403), + + NOT_FOUND(404), + + METHOD_NOT_ALLOWED(405), + + PROXY_AUTHENTICATION_REQUIRED(407), + + INTERNAL_SERVER_ERROR(500), + + SERVICE_UNAVAILABLE(503); + + private final int code; + + HttpResponseStatus(final int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java new file mode 100644 index 0000000000..95db442ec9 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java @@ -0,0 +1,80 @@ +/* + * 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.apache.nifi.web.client.api; + +import java.net.URI; + +/** + * HTTP URI Builder supports construction of a URI using component elements + */ +public interface HttpUriBuilder { + /** + * Build URI based on current component elements + * + * @return URI + */ + URI build(); + + /** + * Set URI scheme as http or https + * + * @param scheme URI scheme + * @return Builder + */ + HttpUriBuilder scheme(String scheme); + + /** + * Set URI host address + * + * @param host Host address + * @return Builder + */ + HttpUriBuilder host(String host); + + /** + * Set URI port number + * + * @param port Port number + * @return Builder + */ + HttpUriBuilder port(int port); + + /** + * Set path with segments encoded according to URL standard requirements + * + * @param encodedPath URL-encoded path + * @return Builder + */ + HttpUriBuilder encodedPath(String encodedPath); + + /** + * Add path segment appending to current path + * + * @param pathSegment Path segment + * @return Builder + */ + HttpUriBuilder addPathSegment(String pathSegment); + + /** + * Add query parameter using specified name and value + * + * @param name Query parameter name + * @param value Query parameter value can be null + * @return Builder + */ + HttpUriBuilder addQueryParameter(String name, String value); +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java new file mode 100644 index 0000000000..feb24af2b6 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java @@ -0,0 +1,37 @@ +/* + * 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.apache.nifi.web.client.api; + +/** + * Enumeration of standard HTTP Request Methods + */ +public enum StandardHttpRequestMethod implements HttpRequestMethod { + DELETE, + + GET, + + PATCH, + + POST, + + PUT; + + @Override + public String getMethod() { + return name(); + } +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java new file mode 100644 index 0000000000..bc33a72da4 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java @@ -0,0 +1,65 @@ +/* + * 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.apache.nifi.web.client.api; + +/** + * Service abstraction for HTTP client operations + */ +public interface WebClientService { + /** + * Create HTTP Request builder starting with specified HTTP Request Method + * + * @param requestMethod HTTP Request Method + * @return HTTP Request URI Specification builder + */ + HttpRequestUriSpec method(HttpRequestMethod requestMethod); + + /** + * Create HTTP Request builder starting with HTTP DELETE + * + * @return HTTP Request URI Specification builder + */ + HttpRequestUriSpec delete(); + + /** + * Create HTTP Request builder starting with HTTP GET + * + * @return HTTP Request URI Specification builder + */ + HttpRequestUriSpec get(); + + /** + * Create HTTP Request builder starting with HTTP PATCH + * + * @return HTTP Request URI Specification builder + */ + HttpRequestUriSpec patch(); + + /** + * Create HTTP Request builder starting with HTTP POST + * + * @return HTTP Request URI Specification builder + */ + HttpRequestUriSpec post(); + + /** + * Create HTTP Request builder starting with HTTP PUT + * + * @return HTTP Request URI Specification builder + */ + HttpRequestUriSpec put(); +} diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java new file mode 100644 index 0000000000..95f7966cb4 --- /dev/null +++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java @@ -0,0 +1,45 @@ +/* + * 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.apache.nifi.web.client.api; + +import java.net.URI; + +/** + * Web Client Service Exception provides a generalized wrapper for HTTP communication failures + */ +public class WebClientServiceException extends RuntimeException { + /** + * Web Service Client Exception with standard HTTP request properties + * + * @param message Failure message + * @param cause Failure cause + * @param uri HTTP Request URI + * @param httpRequestMethod HTTP Request Method + */ + public WebClientServiceException( + final String message, + final Throwable cause, + final URI uri, + final HttpRequestMethod httpRequestMethod + ) { + super(getMessage(message, uri, httpRequestMethod), cause); + } + + private static String getMessage(final String message, final URI uri, final HttpRequestMethod httpRequestMethod) { + return String.format("%s HTTP Method [%s] URI [%s]", message, httpRequestMethod, uri); + } +} diff --git a/nifi-commons/nifi-web-client/pom.xml b/nifi-commons/nifi-web-client/pom.xml new file mode 100644 index 0000000000..ac8563aed6 --- /dev/null +++ b/nifi-commons/nifi-web-client/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-commons + 1.18.0-SNAPSHOT + + nifi-web-client + Standard implementation of nifi-web-client-api using OkHttp + + + + org.apache.nifi + nifi-web-client-api + 1.18.0-SNAPSHOT + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + mockwebserver + test + + + diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java new file mode 100644 index 0000000000..2377812cbb --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java @@ -0,0 +1,43 @@ +/* + * 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.apache.nifi.web.client; + +import okhttp3.Authenticator; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Route; + +/** + * OkHttp Authenticator supporting Proxy Authentication using HTTP Basic credentials + */ +class BasicProxyAuthenticator implements Authenticator { + private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; + + private final String credentials; + + BasicProxyAuthenticator(final String credentials) { + this.credentials = credentials; + } + + @Override + public Request authenticate(final Route route, final Response response) { + return response.request() + .newBuilder() + .header(PROXY_AUTHORIZATION_HEADER, credentials) + .build(); + } +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java new file mode 100644 index 0000000000..68ab5183da --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java @@ -0,0 +1,56 @@ +/* + * 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.apache.nifi.web.client; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; + +import java.io.IOException; +import java.io.InputStream; + +/** + * OkHttp Request Body implementation based on an InputStream + */ +class InputStreamRequestBody extends RequestBody { + private final InputStream inputStream; + + private final long contentLength; + + InputStreamRequestBody(final InputStream inputStream, final long contentLength) { + this.inputStream = inputStream; + this.contentLength = contentLength; + } + + @Override + public long contentLength() { + return contentLength; + } + + @Override + public MediaType contentType() { + return null; + } + + @Override + public void writeTo(final BufferedSink bufferedSink) throws IOException { + final Source source = Okio.source(inputStream); + bufferedSink.writeAll(source); + } +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java new file mode 100644 index 0000000000..26f84e0319 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java @@ -0,0 +1,55 @@ +/* + * 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.apache.nifi.web.client; + +import org.apache.nifi.web.client.api.HttpEntityHeaders; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Standard implementation of HTTP Entity Headers for Standard Web Client Service + */ +class StandardHttpEntityHeaders implements HttpEntityHeaders { + private final Map> headers; + + StandardHttpEntityHeaders(final Map> headers) { + this.headers = Collections.unmodifiableMap(headers); + } + + @Override + public Optional getFirstHeader(final String headerName) { + final List values = getHeader(headerName); + return values.stream().findFirst(); + } + + @Override + public List getHeader(final String headerName) { + Objects.requireNonNull(headerName, "Header Name required"); + final List values = headers.get(headerName); + return values == null ? Collections.emptyList() : Collections.unmodifiableList(values); + } + + @Override + public Collection getHeaderNames() { + return Collections.unmodifiableSet(headers.keySet()); + } +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java new file mode 100644 index 0000000000..c1e8bd1825 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java @@ -0,0 +1,64 @@ +/* + * 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.apache.nifi.web.client; + +import org.apache.nifi.web.client.api.HttpEntityHeaders; +import org.apache.nifi.web.client.api.HttpResponseEntity; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Standard implementation of HTTP Response Entity for Standard Web Client Service + */ +class StandardHttpResponseEntity implements HttpResponseEntity { + private final int statusCode; + + private final HttpEntityHeaders headers; + + private final InputStream body; + + StandardHttpResponseEntity( + final int statusCode, + final HttpEntityHeaders headers, + final InputStream body + ) { + this.statusCode = statusCode; + this.headers = headers; + this.body = body; + } + + @Override + public int statusCode() { + return statusCode; + } + + @Override + public HttpEntityHeaders headers() { + return headers; + } + + @Override + public InputStream body() { + return body; + } + + @Override + public void close() throws IOException { + body.close(); + } +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java new file mode 100644 index 0000000000..33ecfa3f46 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java @@ -0,0 +1,80 @@ +/* + * 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.apache.nifi.web.client; + +import okhttp3.HttpUrl; +import org.apache.nifi.web.client.api.HttpUriBuilder; + +import java.net.URI; +import java.util.Objects; + +/** + * Standard HTTP URI Builder based on OkHttp HttpUrl + */ +public class StandardHttpUriBuilder implements HttpUriBuilder { + private final HttpUrl.Builder builder; + + public StandardHttpUriBuilder() { + this.builder = new HttpUrl.Builder(); + } + + @Override + public URI build() { + final HttpUrl httpUrl = builder.build(); + return httpUrl.uri(); + } + + @Override + public HttpUriBuilder scheme(final String scheme) { + Objects.requireNonNull(scheme, "Scheme required"); + builder.scheme(scheme); + return this; + } + + @Override + public HttpUriBuilder host(final String host) { + Objects.requireNonNull(host, "Host required"); + builder.host(host); + return this; + } + + @Override + public HttpUriBuilder port(int port) { + builder.port(port); + return this; + } + + @Override + public HttpUriBuilder encodedPath(final String encodedPath) { + builder.encodedPath(encodedPath); + return this; + } + + @Override + public HttpUriBuilder addPathSegment(final String pathSegment) { + Objects.requireNonNull(pathSegment, "Path segment required"); + builder.addPathSegment(pathSegment); + return this; + } + + @Override + public HttpUriBuilder addQueryParameter(final String name, final String value) { + Objects.requireNonNull(name, "Parameter name required"); + builder.addQueryParameter(name, value); + return this; + } +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java new file mode 100644 index 0000000000..c0da84130b --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java @@ -0,0 +1,303 @@ +/* + * 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.apache.nifi.web.client; + +import okhttp3.Call; +import okhttp3.Credentials; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +import org.apache.nifi.web.client.api.HttpEntityHeaders; +import org.apache.nifi.web.client.api.HttpRequestBodySpec; +import org.apache.nifi.web.client.api.HttpRequestHeadersSpec; +import org.apache.nifi.web.client.api.HttpRequestMethod; +import org.apache.nifi.web.client.api.HttpRequestUriSpec; +import org.apache.nifi.web.client.api.HttpResponseEntity; +import org.apache.nifi.web.client.api.StandardHttpRequestMethod; +import org.apache.nifi.web.client.api.WebClientService; +import org.apache.nifi.web.client.api.WebClientServiceException; +import org.apache.nifi.web.client.proxy.ProxyContext; +import org.apache.nifi.web.client.redirect.RedirectHandling; +import org.apache.nifi.web.client.ssl.SSLSocketFactoryProvider; +import org.apache.nifi.web.client.ssl.StandardSSLSocketFactoryProvider; +import org.apache.nifi.web.client.ssl.TlsContext; + +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Proxy; +import java.net.URI; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; + +/** + * Standard implementation of Web Client Service using OkHttp + */ +public class StandardWebClientService implements WebClientService { + private static final byte[] EMPTY_BYTES = new byte[0]; + + private static final SSLSocketFactoryProvider sslSocketFactoryProvider = new StandardSSLSocketFactoryProvider(); + + private OkHttpClient okHttpClient; + + /** + * Standard Web Client Service constructor creates OkHttpClient using default settings + */ + public StandardWebClientService() { + okHttpClient = new OkHttpClient.Builder().build(); + } + + /** + * Set timeout for initial socket connection + * + * @param connectTimeout Connect Timeout + */ + public void setConnectTimeout(final Duration connectTimeout) { + Objects.requireNonNull(connectTimeout, "Connect Timeout required"); + okHttpClient = okHttpClient.newBuilder().connectTimeout(connectTimeout).build(); + } + + /** + * Set timeout for reading responses from socket connection + * + * @param readTimeout Read Timeout + */ + public void setReadTimeout(final Duration readTimeout) { + Objects.requireNonNull(readTimeout, "Read Timeout required"); + okHttpClient = okHttpClient.newBuilder().readTimeout(readTimeout).build(); + } + + /** + * Set timeout for writing requests to socket connection + * + * @param writeTimeout Write Timeout + */ + public void setWriteTimeout(final Duration writeTimeout) { + Objects.requireNonNull(writeTimeout, "Write Timeout required"); + okHttpClient = okHttpClient.newBuilder().writeTimeout(writeTimeout).build(); + } + + /** + * Set Proxy Context configuration for socket communication + * + * @param proxyContext Proxy Context configuration + */ + public void setProxyContext(final ProxyContext proxyContext) { + Objects.requireNonNull(proxyContext, "Proxy Context required"); + final Proxy proxy = Objects.requireNonNull(proxyContext.getProxy(), "Proxy required"); + okHttpClient = okHttpClient.newBuilder().proxy(proxy).build(); + + final Optional proxyUsername = proxyContext.getUsername(); + if (proxyUsername.isPresent()) { + final String username = proxyUsername.get(); + final String password = proxyContext.getPassword().orElseThrow(() -> new IllegalArgumentException("Proxy password required")); + final String credentials = Credentials.basic(username, password); + final BasicProxyAuthenticator proxyAuthenticator = new BasicProxyAuthenticator(credentials); + okHttpClient = okHttpClient.newBuilder().proxyAuthenticator(proxyAuthenticator).build(); + } + } + + /** + * Set Redirect Handling strategy + * + * @param redirectHandling Redirect Handling strategy + */ + public void setRedirectHandling(final RedirectHandling redirectHandling) { + Objects.requireNonNull(redirectHandling, "Redirect Handling required"); + final boolean followRedirects = RedirectHandling.FOLLOWED == redirectHandling; + okHttpClient = okHttpClient.newBuilder().followRedirects(followRedirects).followSslRedirects(followRedirects).build(); + } + + /** + * Set TLS Context overrides system default TLS settings for HTTPS communication + * + * @param tlsContext TLS Context + */ + public void setTlsContext(final TlsContext tlsContext) { + Objects.requireNonNull(tlsContext, "TLS Context required"); + final X509TrustManager trustManager = Objects.requireNonNull(tlsContext.getTrustManager(), "Trust Manager required"); + final SSLSocketFactory sslSocketFactory = sslSocketFactoryProvider.getSocketFactory(tlsContext); + okHttpClient = okHttpClient.newBuilder().sslSocketFactory(sslSocketFactory, trustManager).build(); + } + + /** + * Create HTTP Request builder starting with specified HTTP Request Method + * + * @param httpRequestMethod HTTP Request Method required + * @return HTTP Request URI Specification builder + */ + @Override + public HttpRequestUriSpec method(final HttpRequestMethod httpRequestMethod) { + Objects.requireNonNull(httpRequestMethod, "HTTP Request Method required"); + return new StandardHttpRequestUriSpec(httpRequestMethod); + } + + /** + * Create HTTP Request builder starting with HTTP DELETE + * + * @return HTTP Request URI Specification builder + */ + @Override + public HttpRequestUriSpec delete() { + return method(StandardHttpRequestMethod.DELETE); + } + + /** + * Create HTTP Request builder starting with HTTP GET + * + * @return HTTP Request URI Specification builder + */ + @Override + public HttpRequestUriSpec get() { + return method(StandardHttpRequestMethod.GET); + } + + /** + * Create HTTP Request builder starting with HTTP PATCH + * + * @return HTTP Request URI Specification builder + */ + @Override + public HttpRequestUriSpec patch() { + return method(StandardHttpRequestMethod.PATCH); + } + + /** + * Create HTTP Request builder starting with HTTP POST + * + * @return HTTP Request URI Specification builder + */ + public HttpRequestUriSpec post() { + return method(StandardHttpRequestMethod.POST); + } + + /** + * Create HTTP Request builder starting with HTTP PUT + * + * @return HTTP Request URI Specification builder + */ + public HttpRequestUriSpec put() { + return method(StandardHttpRequestMethod.PUT); + } + + class StandardHttpRequestUriSpec implements HttpRequestUriSpec { + private final HttpRequestMethod httpRequestMethod; + + StandardHttpRequestUriSpec(final HttpRequestMethod httpRequestMethod) { + this.httpRequestMethod = httpRequestMethod; + } + + @Override + public HttpRequestBodySpec uri(final URI uri) { + Objects.requireNonNull(uri, "URI required"); + return new StandardHttpRequestBodySpec(httpRequestMethod, uri); + } + } + + class StandardHttpRequestBodySpec implements HttpRequestBodySpec { + private static final long UNKNOWN_CONTENT_LENGTH = -1; + + private final HttpRequestMethod httpRequestMethod; + + private final URI uri; + + private final Headers.Builder headersBuilder; + + private long contentLength = UNKNOWN_CONTENT_LENGTH; + + private InputStream body; + + StandardHttpRequestBodySpec(final HttpRequestMethod httpRequestMethod, final URI uri) { + this.httpRequestMethod = httpRequestMethod; + this.uri = uri; + this.headersBuilder = new Headers.Builder(); + } + + @Override + public HttpRequestHeadersSpec body(final InputStream body, final OptionalLong contentLength) { + this.body = Objects.requireNonNull(body, "Body required"); + this.contentLength = Objects.requireNonNull(contentLength, "Content Length required").orElse(UNKNOWN_CONTENT_LENGTH); + return this; + } + + @Override + public HttpRequestBodySpec header(final String headerName, final String headerValue) { + Objects.requireNonNull(headerName, "Header Name required"); + Objects.requireNonNull(headerValue, "Header Value required"); + headersBuilder.add(headerName, headerValue); + return this; + } + + @Override + public HttpResponseEntity retrieve() { + final Request request = getRequest(); + final Call call = okHttpClient.newCall(request); + final Response response = execute(call); + + final int code = response.code(); + final Headers responseHeaders = response.headers(); + final HttpEntityHeaders headers = new StandardHttpEntityHeaders(responseHeaders.toMultimap()); + final ResponseBody responseBody = response.body(); + final InputStream body = responseBody == null ? new ByteArrayInputStream(EMPTY_BYTES) : responseBody.byteStream(); + + return new StandardHttpResponseEntity(code, headers, body); + } + + private Response execute(final Call call) { + try { + return call.execute(); + } catch (final IOException e) { + throw new WebClientServiceException("Request execution failed", e, uri, httpRequestMethod); + } + } + + private Request getRequest() { + final HttpUrl url = HttpUrl.get(uri); + Objects.requireNonNull(url, "HTTP Request URI required"); + + final Headers headers = headersBuilder.build(); + final RequestBody requestBody = getRequestBody(); + + return new Request.Builder() + .method(httpRequestMethod.getMethod(), requestBody) + .url(url) + .headers(headers) + .build(); + } + + private RequestBody getRequestBody() { + final RequestBody requestBody; + + if (body == null) { + requestBody = null; + } else { + requestBody = new InputStreamRequestBody(body, contentLength); + } + + return requestBody; + } + } +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java new file mode 100644 index 0000000000..570e587189 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java @@ -0,0 +1,46 @@ +/* + * 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.apache.nifi.web.client.proxy; + +import java.net.Proxy; +import java.util.Optional; + +/** + * Proxy Context provides information necessary to access sites through a Proxy with or without authentication + */ +public interface ProxyContext { + /** + * Get Proxy including Proxy Type and Proxy Server + * + * @return Proxy + */ + Proxy getProxy(); + + /** + * Get Username for Proxy Authentication + * + * @return Username or empty when not configured + */ + Optional getUsername(); + + /** + * Get Password for Proxy Authentication + * + * @return Password or empty when not configured + */ + Optional getPassword(); +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java new file mode 100644 index 0000000000..f7f2d23a98 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java @@ -0,0 +1,28 @@ +/* + * 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.apache.nifi.web.client.redirect; + +/** + * HTTP redirect handling strategy + */ +public enum RedirectHandling { + /** Follow HTTP location returned from an HTTP 300 series status */ + FOLLOWED, + + /** Ignore HTTP location returned from an HTTP 300 series status */ + IGNORED +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java new file mode 100644 index 0000000000..a8a0538199 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java @@ -0,0 +1,32 @@ +/* + * 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.apache.nifi.web.client.ssl; + +import javax.net.ssl.SSLSocketFactory; + +/** + * SSLSocketFactory Provider + */ +public interface SSLSocketFactoryProvider { + /** + * Get SSLSocketFactory using provided TLS Context configuration + * + * @param tlsContext TLS Context configuration + * @return SSLSocketFactory + */ + SSLSocketFactory getSocketFactory(TlsContext tlsContext); +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java new file mode 100644 index 0000000000..efa312d214 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java @@ -0,0 +1,70 @@ +/* + * 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.apache.nifi.web.client.ssl; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.Optional; + +/** + * Standard implementation of SSLSocketFactory Provider + */ +public class StandardSSLSocketFactoryProvider implements SSLSocketFactoryProvider { + /** + * Get SSLSocketFactory defaults to system Trust Manager and allows an empty Key Manager + * + * @param tlsContext TLS Context configuration + * @return SSLSocketFactory + */ + @Override + public SSLSocketFactory getSocketFactory(final TlsContext tlsContext) { + Objects.requireNonNull(tlsContext, "TLS Context required"); + final SSLContext sslContext = getSslContext(tlsContext); + + try { + final Optional keyManager = tlsContext.getKeyManager(); + final KeyManager[] keyManagers = keyManager.map(x509KeyManager -> new KeyManager[]{x509KeyManager}).orElse(null); + + final X509TrustManager trustManager = tlsContext.getTrustManager(); + final TrustManager[] trustManagers = trustManager == null ? null : new TrustManager[]{trustManager}; + + final SecureRandom secureRandom = new SecureRandom(); + sslContext.init(keyManagers, trustManagers, secureRandom); + + return sslContext.getSocketFactory(); + } catch (final KeyManagementException e) { + throw new IllegalArgumentException("SSLContext initialization failed", e); + } + } + + private SSLContext getSslContext(final TlsContext tlsContext) { + final String protocol = Objects.requireNonNull(tlsContext.getProtocol(), "TLS Protocol required"); + try { + return SSLContext.getInstance(protocol); + } catch (final NoSuchAlgorithmException e) { + throw new IllegalArgumentException(String.format("SSLContext protocol [%s] not supported", protocol), e); + } + } +} diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java new file mode 100644 index 0000000000..604bfaff21 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java @@ -0,0 +1,47 @@ +/* + * 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.apache.nifi.web.client.ssl; + +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import java.util.Optional; + +/** + * TLS Context provides components necessary for TLS communication + */ +public interface TlsContext { + /** + * Get TLS Protocol + * + * @return TLS Protocol + */ + String getProtocol(); + + /** + * Get X.509 Trust Manager + * + * @return X.509 Trust Manager + */ + X509TrustManager getTrustManager(); + + /** + * Get X.509 Key Manager + * + * @return X.509 Key Manager or empty when not configured + */ + Optional getKeyManager(); +} diff --git a/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java new file mode 100644 index 0000000000..73cb81f0b7 --- /dev/null +++ b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java @@ -0,0 +1,134 @@ +/* + * 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.apache.nifi.web.client; + +import org.apache.nifi.web.client.api.HttpUriBuilder; +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class StandardHttpUriBuilderTest { + private static final String HTTP_SCHEME = "http"; + + private static final String LOCALHOST = "localhost"; + + private static final int PORT = 8080; + + private static final String ENCODED_PATH = "/resources/search"; + + private static final String RESOURCES_PATH_SEGMENT = "resources"; + + private static final String PARAMETER_NAME = "search"; + + private static final String PARAMETER_VALUE = "terms"; + + private static final URI HTTP_LOCALHOST_URI = URI.create(String.format("%s://%s/", HTTP_SCHEME, LOCALHOST)); + + private static final URI HTTP_LOCALHOST_PORT_URI = URI.create(String.format("%s://%s:%d/", HTTP_SCHEME, LOCALHOST, PORT)); + + private static final URI HTTP_LOCALHOST_PORT_ENCODED_PATH_URI = URI.create(String.format("%s://%s:%d%s", HTTP_SCHEME, LOCALHOST, PORT, ENCODED_PATH)); + + private static final URI HTTP_LOCALHOST_RESOURCES_URI = URI.create(String.format("%s%s", HTTP_LOCALHOST_URI, RESOURCES_PATH_SEGMENT)); + + private static final URI HTTP_LOCALHOST_QUERY_URI = URI.create(String.format("%s?%s=%s", HTTP_LOCALHOST_RESOURCES_URI, PARAMETER_NAME, PARAMETER_VALUE)); + + private static final URI HTTP_LOCALHOST_QUERY_EMPTY_VALUE_URI = URI.create(String.format("%s?%s", HTTP_LOCALHOST_RESOURCES_URI, PARAMETER_NAME)); + + @Test + void testBuildIllegalStateException() { + final HttpUriBuilder builder = new StandardHttpUriBuilder(); + + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testBuildSchemeHost() { + final HttpUriBuilder builder = new StandardHttpUriBuilder() + .scheme(HTTP_SCHEME) + .host(LOCALHOST); + + final URI uri = builder.build(); + + assertEquals(HTTP_LOCALHOST_URI, uri); + } + + @Test + void testBuildSchemeHostPort() { + final HttpUriBuilder builder = new StandardHttpUriBuilder() + .scheme(HTTP_SCHEME) + .host(LOCALHOST) + .port(PORT); + + final URI uri = builder.build(); + + assertEquals(HTTP_LOCALHOST_PORT_URI, uri); + } + + @Test + void testBuildSchemeHostPortEncodedPath() { + final HttpUriBuilder builder = new StandardHttpUriBuilder() + .scheme(HTTP_SCHEME) + .host(LOCALHOST) + .port(PORT) + .encodedPath(ENCODED_PATH); + + final URI uri = builder.build(); + + assertEquals(HTTP_LOCALHOST_PORT_ENCODED_PATH_URI, uri); + } + + @Test + void testBuildSchemeHostPathSegment() { + final HttpUriBuilder builder = new StandardHttpUriBuilder() + .scheme(HTTP_SCHEME) + .host(LOCALHOST) + .addPathSegment(RESOURCES_PATH_SEGMENT); + + final URI uri = builder.build(); + + assertEquals(HTTP_LOCALHOST_RESOURCES_URI, uri); + } + + @Test + void testBuildSchemeHostPathSegmentQueryParameter() { + final HttpUriBuilder builder = new StandardHttpUriBuilder() + .scheme(HTTP_SCHEME) + .host(LOCALHOST) + .addPathSegment(RESOURCES_PATH_SEGMENT) + .addQueryParameter(PARAMETER_NAME, PARAMETER_VALUE); + + final URI uri = builder.build(); + + assertEquals(HTTP_LOCALHOST_QUERY_URI, uri); + } + + @Test + void testBuildSchemeHostPathSegmentQueryParameterNullValue() { + final HttpUriBuilder builder = new StandardHttpUriBuilder() + .scheme(HTTP_SCHEME) + .host(LOCALHOST) + .addPathSegment(RESOURCES_PATH_SEGMENT) + .addQueryParameter(PARAMETER_NAME, null); + + final URI uri = builder.build(); + + assertEquals(HTTP_LOCALHOST_QUERY_EMPTY_VALUE_URI, uri); + } +} diff --git a/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java new file mode 100644 index 0000000000..ae0a80fa6f --- /dev/null +++ b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java @@ -0,0 +1,434 @@ +/* + * 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.apache.nifi.web.client; + +import okhttp3.Credentials; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.nifi.web.client.api.HttpEntityHeaders; +import org.apache.nifi.web.client.api.HttpRequestMethod; +import org.apache.nifi.web.client.api.HttpRequestUriSpec; +import org.apache.nifi.web.client.api.HttpResponseEntity; +import org.apache.nifi.web.client.api.HttpResponseStatus; +import org.apache.nifi.web.client.api.StandardHttpRequestMethod; +import org.apache.nifi.web.client.api.WebClientServiceException; +import org.apache.nifi.web.client.proxy.ProxyContext; +import org.apache.nifi.web.client.redirect.RedirectHandling; +import org.apache.nifi.web.client.ssl.TlsContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Proxy; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class StandardWebClientServiceTest { + private static final String ROOT_PATH = "/"; + + private static final String LOCALHOST = "localhost"; + + private static final byte[] EMPTY_BODY = new byte[0]; + + private static final String RESPONSE_BODY = String.class.getSimpleName(); + + private static final byte[] TEXT_BODY = RESPONSE_BODY.getBytes(StandardCharsets.UTF_8); + + private static final Duration FAILURE_TIMEOUT = Duration.ofMillis(100); + + private static final String ACCEPT_HEADER = "Accept"; + + private static final String ACCEPT_ANY_TYPE = "*/*"; + + private static final String CONTENT_LENGTH_HEADER = "content-length"; + + private static final String CONTENT_LENGTH_ZERO = "0"; + + private static final String LOCATION_HEADER = "Location"; + + private static final String PROXY_AUTHENTICATE_HEADER = "Proxy-Authenticate"; + + private static final String PROXY_AUTHENTICATE_BASIC_REALM = "Basic realm=\"Authentication Required\""; + + private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; + + private static final String TLS_PROTOCOL = "TLS"; + + private static final String TLS_PROTOCOL_UNSUPPORTED = "TLSv0"; + + private static final X509Certificate[] TRUSTED_ISSUERS = new X509Certificate[0]; + + @Mock + TlsContext tlsContext; + + @Mock + ProxyContext proxyContext; + + @Mock + X509TrustManager trustManager; + + MockWebServer mockWebServer; + + StandardWebClientService service; + + @BeforeEach + void setServer() { + mockWebServer = new MockWebServer(); + service = new StandardWebClientService(); + } + + @AfterEach + void shutdownServer() throws IOException { + mockWebServer.shutdown(); + } + + @Test + void testSetTlsContext() { + when(tlsContext.getProtocol()).thenReturn(TLS_PROTOCOL); + when(tlsContext.getTrustManager()).thenReturn(trustManager); + when(trustManager.getAcceptedIssuers()).thenReturn(TRUSTED_ISSUERS); + + service.setTlsContext(tlsContext); + } + + @Test + void testSetTlsContextProtocolNotSupported() { + when(tlsContext.getProtocol()).thenReturn(TLS_PROTOCOL_UNSUPPORTED); + when(tlsContext.getTrustManager()).thenReturn(trustManager); + + assertThrows(IllegalArgumentException.class, () -> service.setTlsContext(tlsContext)); + } + + @Test + void testSocketTimeoutException() throws IOException { + mockWebServer.shutdown(); + + service.setConnectTimeout(FAILURE_TIMEOUT); + service.setReadTimeout(FAILURE_TIMEOUT); + service.setWriteTimeout(FAILURE_TIMEOUT); + + when(proxyContext.getProxy()).thenReturn(Proxy.NO_PROXY); + service.setProxyContext(proxyContext); + + final WebClientServiceException exception = assertThrows(WebClientServiceException.class, () -> + service.method(StandardHttpRequestMethod.GET) + .uri(getRootUri()) + .retrieve() + ); + + assertInstanceOf(SocketTimeoutException.class, exception.getCause()); + } + + @Test + void testProxyAuthorization() throws IOException, InterruptedException { + final Proxy proxy = mockWebServer.toProxyAddress(); + when(proxyContext.getProxy()).thenReturn(proxy); + final String username = String.class.getSimpleName(); + final String password = String.class.getName(); + when(proxyContext.getUsername()).thenReturn(Optional.of(username)); + when(proxyContext.getPassword()).thenReturn(Optional.of(password)); + service.setProxyContext(proxyContext); + + mockWebServer.enqueue(new MockResponse() + .setResponseCode(HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.getCode()) + .setHeader(PROXY_AUTHENTICATE_HEADER, PROXY_AUTHENTICATE_BASIC_REALM) + ); + + runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.OK); + + final RecordedRequest proxyAuthorizationRequest = mockWebServer.takeRequest(); + final String proxyAuthorization = proxyAuthorizationRequest.getHeader(PROXY_AUTHORIZATION_HEADER); + final String credentials = Credentials.basic(username, password); + assertEquals(credentials, proxyAuthorization); + } + + @Test + void testRedirectHandlingFollowed() throws InterruptedException { + service.setRedirectHandling(RedirectHandling.FOLLOWED); + + final String location = mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().toString(); + + final MockResponse movedResponse = new MockResponse() + .setResponseCode(HttpResponseStatus.MOVED_PERMANENTLY.getCode()) + .setHeader(LOCATION_HEADER, location); + mockWebServer.enqueue(movedResponse); + + final HttpResponseStatus httpResponseStatus = HttpResponseStatus.OK; + enqueueResponseStatus(httpResponseStatus); + + final HttpResponseEntity httpResponseEntity = service.get() + .uri(getRootUri()) + .retrieve(); + + assertRecordedRequestResponseStatus(httpResponseEntity, StandardHttpRequestMethod.GET, httpResponseStatus); + } + + @Test + void testRedirectHandlingIgnored() throws InterruptedException { + service.setRedirectHandling(RedirectHandling.IGNORED); + + final HttpResponseStatus httpResponseStatus = HttpResponseStatus.MOVED_PERMANENTLY; + + enqueueResponseStatusBody(httpResponseStatus); + + final HttpResponseEntity httpResponseEntity = service.get() + .uri(getRootUri()) + .retrieve(); + + assertRecordedRequestResponseStatus(httpResponseEntity, StandardHttpRequestMethod.GET, httpResponseStatus); + } + + @Test + void testDelete() throws InterruptedException, IOException { + runRequestMethod(service.delete(), StandardHttpRequestMethod.DELETE, HttpResponseStatus.NO_CONTENT); + } + + @Test + void testDeleteMethodNotAllowed() throws InterruptedException, IOException { + runRequestMethod(service.delete(), StandardHttpRequestMethod.DELETE, HttpResponseStatus.METHOD_NOT_ALLOWED); + } + + @Test + void testGet() throws InterruptedException, IOException { + runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.OK); + } + + @Test + void testGetNotFound() throws InterruptedException, IOException { + runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.NOT_FOUND); + } + + @Test + void testGetInternalServerError() throws InterruptedException, IOException { + runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } + + @Test + void testGetServiceUnavailable() throws InterruptedException, IOException { + runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.SERVICE_UNAVAILABLE); + } + + @Test + void testPatch() throws InterruptedException, IOException { + runRequestMethodRequestBody(service.patch(), StandardHttpRequestMethod.PATCH, HttpResponseStatus.OK); + } + + @Test + void testPatchBadRequest() throws InterruptedException, IOException { + runRequestMethodRequestBody(service.patch(), StandardHttpRequestMethod.PATCH, HttpResponseStatus.BAD_REQUEST); + } + + @Test + void testPost() throws InterruptedException, IOException { + runRequestMethodRequestBody(service.post(), StandardHttpRequestMethod.POST, HttpResponseStatus.CREATED); + } + + @Test + void testPostUnauthorized() throws InterruptedException, IOException { + runRequestMethodRequestBody(service.post(), StandardHttpRequestMethod.POST, HttpResponseStatus.UNAUTHORIZED); + } + + @Test + void testPut() throws InterruptedException, IOException { + runRequestMethodRequestBody(service.put(), StandardHttpRequestMethod.PUT, HttpResponseStatus.OK); + } + + @Test + void testPutForbidden() throws InterruptedException, IOException { + runRequestMethodRequestBody(service.put(), StandardHttpRequestMethod.PUT, HttpResponseStatus.FORBIDDEN); + } + + @ParameterizedTest + @EnumSource(value = StandardHttpRequestMethod.class, names = {"DELETE", "GET"}) + void testHttpRequestMethod(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException { + runRequestMethod(service.method(httpRequestMethod), httpRequestMethod, HttpResponseStatus.NO_CONTENT); + } + + @ParameterizedTest + @EnumSource(value = StandardHttpRequestMethod.class, names = {"DELETE", "GET"}) + void testHttpRequestMethodResponseBody(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException { + final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED; + enqueueResponseStatusBody(httpResponseStatus); + + final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod) + .uri(getRootUri()) + .retrieve(); + + assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus); + + try (final InputStream body = httpResponseEntity.body()) { + final byte[] responseBody = new byte[TEXT_BODY.length]; + final int bytesRead = body.read(responseBody); + assertEquals(TEXT_BODY.length, bytesRead); + assertArrayEquals(TEXT_BODY, responseBody); + } + } + + @ParameterizedTest + @EnumSource(value = StandardHttpRequestMethod.class, names = {"PATCH", "POST", "PUT"}) + void testHttpRequestMethodRequestBodyEmpty(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException { + final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED; + enqueueResponseStatus(httpResponseStatus); + + final InputStream body = new ByteArrayInputStream(EMPTY_BODY); + + try (final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod) + .uri(getRootUri()) + .body(body, OptionalLong.empty()) + .retrieve() + ) { + assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus); + assertContentLengthHeaderFound(httpResponseEntity.headers()); + } + } + + @ParameterizedTest + @EnumSource(value = StandardHttpRequestMethod.class, names = {"PATCH", "POST", "PUT"}) + void testHttpRequestMethodRequestBody(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException { + final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED; + enqueueResponseStatus(httpResponseStatus); + + final InputStream body = new ByteArrayInputStream(TEXT_BODY); + + try (final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod) + .uri(getRootUri()) + .header(ACCEPT_HEADER, ACCEPT_ANY_TYPE) + .body(body, OptionalLong.of(TEXT_BODY.length)) + .retrieve() + ) { + + final HttpEntityHeaders headers = httpResponseEntity.headers(); + assertContentLengthHeaderFound(headers); + + final RecordedRequest recordedRequest = assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus); + + assertEquals(TEXT_BODY.length, recordedRequest.getBodySize()); + final byte[] requestBody = recordedRequest.getBody().readByteArray(); + assertArrayEquals(TEXT_BODY, requestBody); + + final String acceptHeader = recordedRequest.getHeader(ACCEPT_HEADER); + assertEquals(ACCEPT_ANY_TYPE, acceptHeader); + } + } + + private void runRequestMethod( + final HttpRequestUriSpec httpRequestUriSpec, + final HttpRequestMethod httpRequestMethod, + final HttpResponseStatus httpResponseStatus + ) throws IOException, InterruptedException { + enqueueResponseStatus(httpResponseStatus); + + try (final HttpResponseEntity httpResponseEntity = httpRequestUriSpec + .uri(getRootUri()) + .retrieve() + ) { + assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus); + assertContentLengthHeaderFound(httpResponseEntity.headers()); + } + } + + + private void runRequestMethodRequestBody( + final HttpRequestUriSpec httpRequestUriSpec, + final HttpRequestMethod httpRequestMethod, + final HttpResponseStatus httpResponseStatus + ) throws IOException, InterruptedException { + enqueueResponseStatus(httpResponseStatus); + + final InputStream body = new ByteArrayInputStream(EMPTY_BODY); + + try (final HttpResponseEntity httpResponseEntity = httpRequestUriSpec + .uri(getRootUri()) + .body(body, OptionalLong.empty()) + .retrieve() + ) { + assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus); + assertContentLengthHeaderFound(httpResponseEntity.headers()); + } + } + + private RecordedRequest assertRecordedRequestResponseStatus( + final HttpResponseEntity httpResponseEntity, + final HttpRequestMethod httpRequestMethod, + final HttpResponseStatus httpResponseStatus + ) throws InterruptedException { + assertNotNull(httpResponseEntity); + + final RecordedRequest recordedRequest = mockWebServer.takeRequest(); + assertEquals(httpRequestMethod.getMethod(), recordedRequest.getMethod()); + + assertEquals(httpResponseStatus.getCode(), httpResponseEntity.statusCode()); + + return recordedRequest; + } + + private void assertContentLengthHeaderFound(final HttpEntityHeaders headers) { + final Optional contentLengthHeader = headers.getFirstHeader(CONTENT_LENGTH_HEADER); + assertTrue(contentLengthHeader.isPresent()); + assertEquals(CONTENT_LENGTH_ZERO, contentLengthHeader.get()); + + final List contentLengthHeaders = headers.getHeader(CONTENT_LENGTH_HEADER); + assertFalse(contentLengthHeaders.isEmpty()); + assertEquals(Collections.singletonList(CONTENT_LENGTH_ZERO), contentLengthHeaders); + + final Collection headerNames = headers.getHeaderNames(); + assertTrue(headerNames.contains(CONTENT_LENGTH_HEADER)); + } + + private void enqueueResponseStatus(final HttpResponseStatus httpResponseStatus) { + mockWebServer.enqueue(new MockResponse().setResponseCode(httpResponseStatus.getCode())); + } + + private void enqueueResponseStatusBody(final HttpResponseStatus httpResponseStatus) { + mockWebServer.enqueue(new MockResponse() + .setResponseCode(httpResponseStatus.getCode()) + .setBody(RESPONSE_BODY) + ); + } + + private URI getRootUri() { + return mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().uri(); + } +} diff --git a/nifi-commons/pom.xml b/nifi-commons/pom.xml index 9c16bdc2be..1789e2fe4f 100644 --- a/nifi-commons/pom.xml +++ b/nifi-commons/pom.xml @@ -64,6 +64,8 @@ nifi-utils nifi-uuid5 nifi-vault-utils + nifi-web-client + nifi-web-client-api nifi-web-utils nifi-write-ahead-log nifi-xml-processing diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml index 0e76307c87..acbd7e46b3 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml +++ b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml @@ -127,5 +127,11 @@ 1.18.0-SNAPSHOT compile + + org.apache.nifi + nifi-web-client-provider-api + 1.18.0-SNAPSHOT + compile + diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml new file mode 100644 index 0000000000..57b93c5b62 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-web-client-provider-bundle + 1.18.0-SNAPSHOT + + nifi-web-client-provider-api + + + org.apache.nifi + nifi-web-client-api + 1.18.0-SNAPSHOT + + + org.apache.nifi + nifi-api + + + diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java new file mode 100644 index 0000000000..76382053f5 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java @@ -0,0 +1,40 @@ +/* + * 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.apache.nifi.web.client.provider.api; + +import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.web.client.api.HttpUriBuilder; +import org.apache.nifi.web.client.api.WebClientService; + +/** + * Web Client Service Provider abstracts configuration of Web Client Service instances + */ +public interface WebClientServiceProvider extends ControllerService { + /** + * Get new HTTP URI Builder + * + * @return New instance of HTTP URI Builder + */ + HttpUriBuilder getHttpUriBuilder(); + + /** + * Get Web Client Service based on current configuration + * + * @return Configured Web Client Service + */ + WebClientService getWebClientService(); +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml new file mode 100644 index 0000000000..f7ee3f865c --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-web-client-provider-bundle + 1.18.0-SNAPSHOT + + nifi-web-client-provider-service-nar + nar + + + org.apache.nifi + nifi-web-client-provider-service + 1.18.0-SNAPSHOT + + + org.apache.nifi + nifi-standard-services-api-nar + 1.18.0-SNAPSHOT + nar + + + diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml new file mode 100644 index 0000000000..e4aa47b172 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml @@ -0,0 +1,72 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-web-client-provider-bundle + 1.18.0-SNAPSHOT + + nifi-web-client-provider-service + + + org.apache.nifi + nifi-web-client-provider-api + 1.18.0-SNAPSHOT + provided + + + org.apache.nifi + nifi-ssl-context-service-api + provided + + + org.apache.nifi + nifi-proxy-configuration-api + provided + + + org.apache.nifi + nifi-web-client-api + 1.18.0-SNAPSHOT + + + org.apache.nifi + nifi-web-client + 1.18.0-SNAPSHOT + + + org.apache.nifi + nifi-api + + + org.apache.nifi + nifi-utils + 1.18.0-SNAPSHOT + + + org.apache.nifi + nifi-mock + 1.18.0-SNAPSHOT + test + + + com.squareup.okhttp3 + mockwebserver + test + + + diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java new file mode 100644 index 0000000000..9d6dddff45 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java @@ -0,0 +1,35 @@ +/* + * 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.apache.nifi.web.client.provider.service; + +import org.apache.nifi.ssl.SSLContextService; + +import javax.net.ssl.X509KeyManager; +import java.util.Optional; + +/** + * Provider abstraction for loading a Key Manager + */ +interface KeyManagerProvider { + /** + * Get X.509 Key Manager + * + * @param sslContextService SSL Context Service + * @return X.509 Key Manager or empty when not configured + */ + Optional getKeyManager(SSLContextService sslContextService); +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java new file mode 100644 index 0000000000..3db552494b --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java @@ -0,0 +1,109 @@ +/* + * 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.apache.nifi.web.client.provider.service; + +import org.apache.nifi.ssl.SSLContextService; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509KeyManager; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Optional; + +/** + * Standard implementation of Key Manager Provider + */ +class StandardKeyManagerProvider implements KeyManagerProvider { + /** + * Get X.509 Key Manager using SSL Context Service configuration properties + * + * @param sslContextService SSL Context Service + * @return X.509 Key Manager or empty when not configured + */ + @Override + public Optional getKeyManager(final SSLContextService sslContextService) { + final X509KeyManager keyManager; + + if (sslContextService.isKeyStoreConfigured()) { + final KeyManagerFactory keyManagerFactory = getKeyManagerFactory(); + final KeyStore keyStore = getKeyStore(sslContextService); + final char[] keyPassword = getKeyPassword(sslContextService); + try { + keyManagerFactory.init(keyStore, keyPassword); + } catch (final KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new IllegalStateException("Key Manager Factory initialization failed", e); + } + + final KeyManager[] keyManagers = keyManagerFactory.getKeyManagers(); + final Optional firstKeyManager = Arrays.stream(keyManagers).findFirst(); + final KeyManager configuredKeyManager = firstKeyManager.orElse(null); + keyManager = configuredKeyManager instanceof X509KeyManager ? (X509KeyManager) configuredKeyManager : null; + } else { + keyManager = null; + } + + return Optional.ofNullable(keyManager); + } + + private KeyStore getKeyStore(final SSLContextService sslContextService) { + final String keyStoreType = sslContextService.getKeyStoreType(); + final KeyStore keyStore = getKeyStore(keyStoreType); + final char[] keyStorePassword = sslContextService.getKeyStorePassword().toCharArray(); + final String keyStoreFile = sslContextService.getKeyStoreFile(); + try { + try (final InputStream inputStream = new FileInputStream(keyStoreFile)) { + keyStore.load(inputStream, keyStorePassword); + } + return keyStore; + } catch (final IOException e) { + throw new IllegalStateException(String.format("Key Store File [%s] reading failed", keyStoreFile), e); + } catch (final NoSuchAlgorithmException | CertificateException e) { + throw new IllegalStateException(String.format("Key Store File [%s] loading failed", keyStoreFile), e); + } + } + + private KeyStore getKeyStore(final String keyStoreType) { + try { + return KeyStore.getInstance(keyStoreType); + } catch (final KeyStoreException e) { + throw new IllegalStateException(String.format("Key Store Type [%s] creation failed", keyStoreType), e); + } + } + + private char[] getKeyPassword(final SSLContextService sslContextService) { + final String keyPassword = sslContextService.getKeyPassword(); + final String keyStorePassword = sslContextService.getKeyStorePassword(); + final String password = keyPassword == null ? keyStorePassword : keyPassword; + return password.toCharArray(); + } + + private KeyManagerFactory getKeyManagerFactory() { + try { + return KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + } catch (final NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Key Manager Factory creation failed", e); + } + } +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java new file mode 100644 index 0000000000..fc51ee8f1a --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java @@ -0,0 +1,206 @@ +/* + * 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.apache.nifi.web.client.provider.service; + +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.PropertyValue; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.proxy.ProxyConfiguration; +import org.apache.nifi.proxy.ProxyConfigurationService; +import org.apache.nifi.ssl.SSLContextService; +import org.apache.nifi.web.client.StandardHttpUriBuilder; +import org.apache.nifi.web.client.api.HttpUriBuilder; +import org.apache.nifi.web.client.proxy.ProxyContext; +import org.apache.nifi.web.client.StandardWebClientService; +import org.apache.nifi.web.client.redirect.RedirectHandling; +import org.apache.nifi.web.client.ssl.TlsContext; +import org.apache.nifi.web.client.api.WebClientService; +import org.apache.nifi.web.client.provider.api.WebClientServiceProvider; + +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import java.net.Proxy; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static org.apache.nifi.proxy.ProxyConfigurationService.PROXY_CONFIGURATION_SERVICE; + +@CapabilityDescription("Web Client Service Provider with support for configuring standard HTTP connection properties") +@Tags({ "HTTP", "Web", "Client" }) +public class StandardWebClientServiceProvider extends AbstractControllerService implements WebClientServiceProvider { + + static final PropertyDescriptor CONNECT_TIMEOUT = new PropertyDescriptor.Builder() + .name("connect-timeout") + .displayName("Connect Timeout") + .description("Maximum amount of time to wait before failing during initial socket connection") + .required(true) + .defaultValue("10 secs") + .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) + .build(); + + static final PropertyDescriptor READ_TIMEOUT = new PropertyDescriptor.Builder() + .name("read-timeout") + .displayName("Read Timeout") + .description("Maximum amount of time to wait before failing while reading socket responses") + .required(true) + .defaultValue("10 secs") + .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) + .build(); + + static final PropertyDescriptor WRITE_TIMEOUT = new PropertyDescriptor.Builder() + .name("write-timeout") + .displayName("Write Timeout") + .description("Maximum amount of time to wait before failing while writing socket requests") + .required(true) + .defaultValue("10 secs") + .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) + .build(); + + static final PropertyDescriptor REDIRECT_HANDLING_STRATEGY = new PropertyDescriptor.Builder() + .name("redirect-handling-strategy") + .displayName("Redirect Handling Strategy") + .description("Handling strategy for responding to HTTP 301 or 302 redirects received with a Location header") + .required(true) + .defaultValue(RedirectHandling.FOLLOWED.name()) + .allowableValues(RedirectHandling.values()) + .build(); + + static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder() + .name("ssl-context-service") + .displayName("SSL Context Service") + .description("SSL Context Service overrides system default TLS settings for HTTPS communication") + .required(false) + .identifiesControllerService(SSLContextService.class) + .build(); + + static final List PROPERTY_DESCRIPTORS = Arrays.asList( + CONNECT_TIMEOUT, + READ_TIMEOUT, + WRITE_TIMEOUT, + REDIRECT_HANDLING_STRATEGY, + SSL_CONTEXT_SERVICE, + PROXY_CONFIGURATION_SERVICE + ); + + private static final KeyManagerProvider keyManagerProvider = new StandardKeyManagerProvider(); + + private WebClientService webClientService; + + @OnEnabled + public void onEnabled(final ConfigurationContext context) { + final StandardWebClientService standardWebClientService = new StandardWebClientService(); + + final Duration connectTimeout = getDuration(context, CONNECT_TIMEOUT); + standardWebClientService.setConnectTimeout(connectTimeout); + + final Duration readTimeout = getDuration(context, READ_TIMEOUT); + standardWebClientService.setReadTimeout(readTimeout); + + final Duration writeTimeout = getDuration(context, WRITE_TIMEOUT); + standardWebClientService.setReadTimeout(writeTimeout); + + final String redirectHandlingStrategy = context.getProperty(REDIRECT_HANDLING_STRATEGY).getValue(); + final RedirectHandling redirectHandling = RedirectHandling.valueOf(redirectHandlingStrategy); + standardWebClientService.setRedirectHandling(redirectHandling); + + final PropertyValue sslContextServiceProperty = context.getProperty(SSL_CONTEXT_SERVICE); + if (sslContextServiceProperty.isSet()) { + final SSLContextService sslContextService = sslContextServiceProperty.asControllerService(SSLContextService.class); + final TlsContext tlsContext = getTlsContext(sslContextService); + standardWebClientService.setTlsContext(tlsContext); + } + + final PropertyValue proxyConfigurationServiceProperty = context.getProperty(PROXY_CONFIGURATION_SERVICE); + if (proxyConfigurationServiceProperty.isSet()) { + final ProxyConfigurationService proxyConfigurationService = context.getProperty(PROXY_CONFIGURATION_SERVICE).asControllerService(ProxyConfigurationService.class); + final ProxyConfiguration proxyConfiguration = proxyConfigurationService.getConfiguration(); + final ProxyContext proxyContext = getProxyContext(proxyConfiguration); + standardWebClientService.setProxyContext(proxyContext); + } + + webClientService = standardWebClientService; + } + + @Override + public HttpUriBuilder getHttpUriBuilder() { + return new StandardHttpUriBuilder(); + } + + @Override + public WebClientService getWebClientService() { + return webClientService; + } + + @Override + protected List getSupportedPropertyDescriptors() { + return PROPERTY_DESCRIPTORS; + } + + private Duration getDuration(final ConfigurationContext context, final PropertyDescriptor propertyDescriptor) { + final long millis = context.getProperty(propertyDescriptor).asTimePeriod(TimeUnit.MILLISECONDS); + return Duration.ofMillis(millis); + } + + private TlsContext getTlsContext(final SSLContextService sslContextService) { + final X509TrustManager trustManager = sslContextService.createTrustManager(); + final Optional keyManager = keyManagerProvider.getKeyManager(sslContextService); + + return new TlsContext() { + @Override + public String getProtocol() { + return sslContextService.getSslAlgorithm(); + } + + @Override + public X509TrustManager getTrustManager() { + return trustManager; + } + + @Override + public Optional getKeyManager() { + return keyManager; + } + }; + } + + private ProxyContext getProxyContext(final ProxyConfiguration proxyConfiguration) { + return new ProxyContext() { + @Override + public Proxy getProxy() { + return proxyConfiguration.createProxy(); + } + + @Override + public Optional getUsername() { + return Optional.ofNullable(proxyConfiguration.getProxyUserName()); + } + + @Override + public Optional getPassword() { + return Optional.ofNullable(proxyConfiguration.getProxyUserPassword()); + } + }; + } +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService new file mode 100644 index 0000000000..53db7395ea --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService @@ -0,0 +1,15 @@ +# 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. +org.apache.nifi.web.client.provider.service.StandardWebClientServiceProvider diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java new file mode 100644 index 0000000000..ccb6601263 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java @@ -0,0 +1,76 @@ +/* + * 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.apache.nifi.web.client.provider.service; + +import org.apache.nifi.security.util.TemporaryKeyStoreBuilder; +import org.apache.nifi.security.util.TlsConfiguration; +import org.apache.nifi.ssl.SSLContextService; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.net.ssl.X509KeyManager; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class StandardKeyManagerProviderTest { + static TlsConfiguration tlsConfiguration; + + @Mock + SSLContextService sslContextService; + + StandardKeyManagerProvider provider; + + @BeforeAll + static void setTlsConfiguration() { + tlsConfiguration = new TemporaryKeyStoreBuilder().build(); + } + + @BeforeEach + void setProvider() { + provider = new StandardKeyManagerProvider(); + } + + @Test + void testGetKeyManagerNotConfigured() { + when(sslContextService.isKeyStoreConfigured()).thenReturn(false); + + final Optional keyManager = provider.getKeyManager(sslContextService); + + assertFalse(keyManager.isPresent()); + } + + @Test + void testGetKeyManager() { + when(sslContextService.isKeyStoreConfigured()).thenReturn(true); + when(sslContextService.getKeyStoreType()).thenReturn(tlsConfiguration.getKeystoreType().getType()); + when(sslContextService.getKeyStoreFile()).thenReturn(tlsConfiguration.getKeystorePath()); + when(sslContextService.getKeyStorePassword()).thenReturn(tlsConfiguration.getKeystorePassword()); + + final Optional keyManager = provider.getKeyManager(sslContextService); + + assertTrue(keyManager.isPresent()); + } +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java new file mode 100644 index 0000000000..2c74b10e44 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java @@ -0,0 +1,245 @@ +/* + * 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.apache.nifi.web.client.provider.service; + +import okhttp3.Credentials; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.nifi.proxy.ProxyConfiguration; +import org.apache.nifi.proxy.ProxyConfigurationService; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.security.util.SslContextFactory; +import org.apache.nifi.security.util.TemporaryKeyStoreBuilder; +import org.apache.nifi.security.util.TlsConfiguration; +import org.apache.nifi.security.util.TlsException; +import org.apache.nifi.ssl.SSLContextService; +import org.apache.nifi.util.NoOpProcessor; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.apache.nifi.web.client.api.HttpResponseEntity; +import org.apache.nifi.web.client.api.HttpResponseStatus; +import org.apache.nifi.web.client.api.HttpUriBuilder; +import org.apache.nifi.web.client.api.WebClientService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class StandardWebClientServiceProviderTest { + private static final String SERVICE_ID = StandardWebClientServiceProvider.class.getSimpleName(); + + private static final String SSL_CONTEXT_SERVICE_ID = SSLContextService.class.getSimpleName(); + + private static final String PROXY_SERVICE_ID = ProxyConfigurationService.class.getSimpleName(); + + private static final String LOCALHOST = "localhost"; + + private static final String HTTPS = "https"; + + private static final int PORT = 8443; + + private static final String PATH_SEGMENT = "resources"; + + private static final String PARAMETER_NAME = "search"; + + private static final String PARAMETER_VALUE = "search"; + + private static final String ROOT_PATH = "/"; + + private static final URI LOCALHOST_URI = URI.create(String.format("%s://%s:%d/%s?%s=%s", HTTPS, LOCALHOST, PORT, PATH_SEGMENT, PARAMETER_NAME, PARAMETER_VALUE)); + + private static final String PROXY_AUTHENTICATE_HEADER = "Proxy-Authenticate"; + + private static final String PROXY_AUTHENTICATE_BASIC_REALM = "Basic realm=\"Authentication Required\""; + + private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; + + private static final boolean TUNNEL_PROXY_DISABLED = false; + + static TlsConfiguration tlsConfiguration; + + static SSLContext sslContext; + + static X509TrustManager trustManager; + + @Mock + SSLContextService sslContextService; + + @Mock + ProxyConfigurationService proxyConfigurationService; + + TestRunner runner; + + MockWebServer mockWebServer; + + StandardWebClientServiceProvider provider; + + @BeforeAll + static void setTlsConfiguration() throws TlsException { + tlsConfiguration = new TemporaryKeyStoreBuilder().build(); + sslContext = SslContextFactory.createSslContext(tlsConfiguration); + trustManager = SslContextFactory.getX509TrustManager(tlsConfiguration); + } + + @BeforeEach + void setRunner() throws InitializationException { + mockWebServer = new MockWebServer(); + + runner = TestRunners.newTestRunner(NoOpProcessor.class); + + provider = new StandardWebClientServiceProvider(); + runner.addControllerService(SERVICE_ID, provider); + } + + @AfterEach + void shutdownServer() throws IOException { + mockWebServer.shutdown(); + } + + @Test + void testEnable() { + runner.enableControllerService(provider); + } + + @Test + void testGetHttpUriBuilder() { + runner.enableControllerService(provider); + + final HttpUriBuilder httpUriBuilder = provider.getHttpUriBuilder(); + + final URI uri = httpUriBuilder.scheme(HTTPS) + .host(LOCALHOST) + .port(PORT) + .addPathSegment(PATH_SEGMENT) + .addQueryParameter(PARAMETER_NAME, PARAMETER_VALUE) + .build(); + + assertEquals(LOCALHOST_URI, uri); + } + + @Test + void testGetWebServiceClientGetUri() throws InterruptedException { + runner.enableControllerService(provider); + + final WebClientService webClientService = provider.getWebClientService(); + + assertNotNull(webClientService); + + assertGetUriCompleted(webClientService); + } + + @Test + void testGetWebServiceClientSslContextServiceConfiguredGetUri() throws InitializationException, InterruptedException { + when(sslContextService.getIdentifier()).thenReturn(SSL_CONTEXT_SERVICE_ID); + when(sslContextService.getSslAlgorithm()).thenReturn(tlsConfiguration.getProtocol()); + when(sslContextService.createTrustManager()).thenReturn(trustManager); + + runner.addControllerService(SSL_CONTEXT_SERVICE_ID, sslContextService); + runner.enableControllerService(sslContextService); + + runner.setProperty(provider, StandardWebClientServiceProvider.SSL_CONTEXT_SERVICE, SSL_CONTEXT_SERVICE_ID); + runner.enableControllerService(provider); + + final WebClientService webClientService = provider.getWebClientService(); + + assertNotNull(webClientService); + + final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + mockWebServer.useHttps(sslSocketFactory, TUNNEL_PROXY_DISABLED); + + assertGetUriCompleted(webClientService); + } + + @Test + void testGetWebServiceClientProxyConfigurationGetUri() throws InitializationException, InterruptedException { + final Proxy proxy = mockWebServer.toProxyAddress(); + final InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); + + final ProxyConfiguration proxyConfiguration = new ProxyConfiguration(); + proxyConfiguration.setProxyType(Proxy.Type.HTTP); + proxyConfiguration.setProxyServerHost(proxyAddress.getHostName()); + proxyConfiguration.setProxyServerPort(proxyAddress.getPort()); + + final String username = String.class.getSimpleName(); + final String password = String.class.getName(); + proxyConfiguration.setProxyUserName(username); + proxyConfiguration.setProxyUserPassword(password); + + when(proxyConfigurationService.getIdentifier()).thenReturn(PROXY_SERVICE_ID); + when(proxyConfigurationService.getConfiguration()).thenReturn(proxyConfiguration); + + mockWebServer.enqueue(new MockResponse() + .setResponseCode(HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.getCode()) + .setHeader(PROXY_AUTHENTICATE_HEADER, PROXY_AUTHENTICATE_BASIC_REALM) + ); + + runner.addControllerService(PROXY_SERVICE_ID, proxyConfigurationService); + runner.enableControllerService(proxyConfigurationService); + + runner.setProperty(provider, ProxyConfigurationService.PROXY_CONFIGURATION_SERVICE, PROXY_SERVICE_ID); + runner.enableControllerService(provider); + + final WebClientService webClientService = provider.getWebClientService(); + + assertNotNull(webClientService); + + assertGetUriCompleted(webClientService); + + final RecordedRequest proxyAuthorizationRequest = mockWebServer.takeRequest(); + final String proxyAuthorization = proxyAuthorizationRequest.getHeader(PROXY_AUTHORIZATION_HEADER); + final String credentials = Credentials.basic(username, password); + assertEquals(credentials, proxyAuthorization); + } + + private void assertGetUriCompleted(final WebClientService webClientService) throws InterruptedException { + final URI uri = mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().uri(); + + final HttpResponseStatus httpResponseStatus = HttpResponseStatus.OK; + final MockResponse mockResponse = new MockResponse().setResponseCode(httpResponseStatus.getCode()); + mockWebServer.enqueue(mockResponse); + + final HttpResponseEntity httpResponseEntity = webClientService.get().uri(uri).retrieve(); + + assertNotNull(httpResponseEntity); + assertEquals(httpResponseStatus.getCode(), httpResponseEntity.statusCode()); + + final RecordedRequest request = mockWebServer.takeRequest(); + final HttpUrl requestUrl = request.getRequestUrl(); + assertNotNull(requestUrl); + + final URI requestUri = requestUrl.uri(); + assertEquals(uri.getPort(), requestUri.getPort()); + } +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml new file mode 100644 index 0000000000..6db83509db --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-standard-services + 1.18.0-SNAPSHOT + + nifi-web-client-provider-bundle + pom + + nifi-web-client-provider-api + nifi-web-client-provider-service + nifi-web-client-provider-service-nar + + diff --git a/nifi-nar-bundles/nifi-standard-services/pom.xml b/nifi-nar-bundles/nifi-standard-services/pom.xml index 524fc4fbb3..6e210e8f7d 100644 --- a/nifi-nar-bundles/nifi-standard-services/pom.xml +++ b/nifi-nar-bundles/nifi-standard-services/pom.xml @@ -54,5 +54,6 @@ nifi-hadoop-dbcp-service-bundle nifi-kerberos-user-service-api nifi-kerberos-user-service-bundle + nifi-web-client-provider-bundle