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 <greyp@apache.org>
This commit is contained in:
exceptionfactory 2022-07-19 23:15:57 -05:00 committed by Paul Grey
parent a3a7b2fcfd
commit 864036674e
No known key found for this signature in database
GPG Key ID: 8DDF32B9C7EE39D0
41 changed files with 2888 additions and 0 deletions

View File

@ -844,6 +844,12 @@ language governing permissions and limitations under the License. -->
<version>1.18.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-provider-service-nar</artifactId>
<version>1.18.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<!-- dependencies for jaxb/activation/annotation for running NiFi on Java 11 -->
<!-- TODO: remove these once minimum Java version is 11 -->
<dependency>

View File

@ -0,0 +1,25 @@
<?xml version="1.0"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-web-client-api</artifactId>
<description>Abstracts standard HTTP client operations without depending on a specific HTTP library</description>
</project>

View File

@ -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<String> 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<String> getHeader(String headerName);
/**
* Get Header Names
*
* @return Collection of Header Names or empty when not found
*/
Collection<String> getHeaderNames();
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-web-client</artifactId>
<description>Standard implementation of nifi-web-client-api using OkHttp</description>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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<String, List<String>> headers;
StandardHttpEntityHeaders(final Map<String, List<String>> headers) {
this.headers = Collections.unmodifiableMap(headers);
}
@Override
public Optional<String> getFirstHeader(final String headerName) {
final List<String> values = getHeader(headerName);
return values.stream().findFirst();
}
@Override
public List<String> getHeader(final String headerName) {
Objects.requireNonNull(headerName, "Header Name required");
final List<String> values = headers.get(headerName);
return values == null ? Collections.emptyList() : Collections.unmodifiableList(values);
}
@Override
public Collection<String> getHeaderNames() {
return Collections.unmodifiableSet(headers.keySet());
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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<String> 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;
}
}
}

View File

@ -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<String> getUsername();
/**
* Get Password for Proxy Authentication
*
* @return Password or empty when not configured
*/
Optional<String> getPassword();
}

View File

@ -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
}

View File

@ -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);
}

View File

@ -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<X509KeyManager> 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);
}
}
}

View File

@ -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<X509KeyManager> getKeyManager();
}

View File

@ -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);
}
}

View File

@ -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<String> contentLengthHeader = headers.getFirstHeader(CONTENT_LENGTH_HEADER);
assertTrue(contentLengthHeader.isPresent());
assertEquals(CONTENT_LENGTH_ZERO, contentLengthHeader.get());
final List<String> contentLengthHeaders = headers.getHeader(CONTENT_LENGTH_HEADER);
assertFalse(contentLengthHeaders.isEmpty());
assertEquals(Collections.singletonList(CONTENT_LENGTH_ZERO), contentLengthHeaders);
final Collection<String> 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();
}
}

View File

@ -64,6 +64,8 @@
<module>nifi-utils</module>
<module>nifi-uuid5</module>
<module>nifi-vault-utils</module>
<module>nifi-web-client</module>
<module>nifi-web-client-api</module>
<module>nifi-web-utils</module>
<module>nifi-write-ahead-log</module>
<module>nifi-xml-processing</module>

View File

@ -127,5 +127,11 @@
<version>1.18.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-provider-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,35 @@
<?xml version="1.0"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-provider-bundle</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-web-client-provider-api</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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();
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-provider-bundle</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-web-client-provider-service-nar</artifactId>
<packaging>nar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-provider-service</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-standard-services-api-nar</artifactId>
<version>1.18.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,72 @@
<?xml version="1.0"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-provider-bundle</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-web-client-provider-service</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-provider-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-ssl-context-service-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-proxy-configuration-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-client</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<X509KeyManager> getKeyManager(SSLContextService sslContextService);
}

View File

@ -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<X509KeyManager> 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<KeyManager> 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);
}
}
}

View File

@ -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<PropertyDescriptor> 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<PropertyDescriptor> 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<X509KeyManager> keyManager = keyManagerProvider.getKeyManager(sslContextService);
return new TlsContext() {
@Override
public String getProtocol() {
return sslContextService.getSslAlgorithm();
}
@Override
public X509TrustManager getTrustManager() {
return trustManager;
}
@Override
public Optional<X509KeyManager> getKeyManager() {
return keyManager;
}
};
}
private ProxyContext getProxyContext(final ProxyConfiguration proxyConfiguration) {
return new ProxyContext() {
@Override
public Proxy getProxy() {
return proxyConfiguration.createProxy();
}
@Override
public Optional<String> getUsername() {
return Optional.ofNullable(proxyConfiguration.getProxyUserName());
}
@Override
public Optional<String> getPassword() {
return Optional.ofNullable(proxyConfiguration.getProxyUserPassword());
}
};
}
}

View File

@ -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

View File

@ -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<X509KeyManager> 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<X509KeyManager> keyManager = provider.getKeyManager(sslContextService);
assertTrue(keyManager.isPresent());
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-standard-services</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-web-client-provider-bundle</artifactId>
<packaging>pom</packaging>
<modules>
<module>nifi-web-client-provider-api</module>
<module>nifi-web-client-provider-service</module>
<module>nifi-web-client-provider-service-nar</module>
</modules>
</project>

View File

@ -54,5 +54,6 @@
<module>nifi-hadoop-dbcp-service-bundle</module>
<module>nifi-kerberos-user-service-api</module>
<module>nifi-kerberos-user-service-bundle</module>
<module>nifi-web-client-provider-bundle</module>
</modules>
</project>