/* * 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.bootstrap.notification.http; import okhttp3.Call; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import org.apache.nifi.bootstrap.notification.AbstractNotificationService; import org.apache.nifi.bootstrap.notification.NotificationContext; import org.apache.nifi.bootstrap.notification.NotificationFailedException; import org.apache.nifi.bootstrap.notification.NotificationInitializationContext; import org.apache.nifi.bootstrap.notification.NotificationType; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.PropertyValue; import org.apache.nifi.expression.AttributeExpression; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.security.util.SslContextFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; public class HttpNotificationService extends AbstractNotificationService { public static final String NOTIFICATION_TYPE_KEY = "notification.type"; public static final String NOTIFICATION_SUBJECT_KEY = "notification.subject"; public static final String STORE_TYPE_JKS = "JKS"; public static final String STORE_TYPE_PKCS12 = "PKCS12"; public static final PropertyDescriptor PROP_URL = new PropertyDescriptor.Builder() .name("URL") .description("The URL to send the notification to.") .required(true) .expressionLanguageSupported(true) .addValidator(StandardValidators.URL_VALIDATOR) .build(); public static final PropertyDescriptor PROP_CONNECTION_TIMEOUT = new PropertyDescriptor.Builder() .name("Connection timeout") .description("Max wait time for connection to remote service.") .expressionLanguageSupported(true) .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) .defaultValue("10s") .build(); public static final PropertyDescriptor PROP_WRITE_TIMEOUT = new PropertyDescriptor.Builder() .name("Write timeout") .description("Max wait time for remote service to read the request sent.") .expressionLanguageSupported(true) .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) .defaultValue("10s") .build(); public static final PropertyDescriptor PROP_TRUSTSTORE = new PropertyDescriptor.Builder() .name("Truststore Filename") .description("The fully-qualified filename of the Truststore") .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR) .sensitive(false) .build(); public static final PropertyDescriptor PROP_TRUSTSTORE_TYPE = new PropertyDescriptor.Builder() .name("Truststore Type") .description("The Type of the Truststore. Either JKS or PKCS12") .allowableValues(STORE_TYPE_JKS, STORE_TYPE_PKCS12) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(false) .build(); public static final PropertyDescriptor PROP_TRUSTSTORE_PASSWORD = new PropertyDescriptor.Builder() .name("Truststore Password") .description("The password for the Truststore") .defaultValue(null) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(true) .build(); public static final PropertyDescriptor PROP_KEYSTORE = new PropertyDescriptor.Builder() .name("Keystore Filename") .description("The fully-qualified filename of the Keystore") .defaultValue(null) .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR) .sensitive(false) .build(); public static final PropertyDescriptor PROP_KEYSTORE_TYPE = new PropertyDescriptor.Builder() .name("Keystore Type") .description("The Type of the Keystore") .allowableValues(STORE_TYPE_JKS, STORE_TYPE_PKCS12) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(false) .build(); public static final PropertyDescriptor PROP_KEYSTORE_PASSWORD = new PropertyDescriptor.Builder() .name("Keystore Password") .defaultValue(null) .description("The password for the Keystore") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(true) .build(); public static final PropertyDescriptor PROP_KEY_PASSWORD = new PropertyDescriptor.Builder() .name("Key Password") .displayName("Key Password") .description("The password for the key. If this is not specified, but the Keystore Filename, Password, and Type are specified, " + "then the Keystore Password will be assumed to be the same as the Key Password.") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(true) .build(); public static final PropertyDescriptor SSL_ALGORITHM = new PropertyDescriptor.Builder() .name("SSL Protocol") .defaultValue("TLS") .allowableValues("SSL", "TLS") .description("The algorithm to use for this SSL context.") .sensitive(false) .build(); private final AtomicReference<OkHttpClient> httpClientReference = new AtomicReference<>(); private final AtomicReference<String> urlReference = new AtomicReference<>(); private static final List<PropertyDescriptor> supportedProperties; static { supportedProperties = new ArrayList<>(); supportedProperties.add(PROP_URL); supportedProperties.add(PROP_CONNECTION_TIMEOUT); supportedProperties.add(PROP_WRITE_TIMEOUT); supportedProperties.add(PROP_TRUSTSTORE); supportedProperties.add(PROP_TRUSTSTORE_PASSWORD); supportedProperties.add(PROP_TRUSTSTORE_TYPE); supportedProperties.add(PROP_KEYSTORE); supportedProperties.add(PROP_KEYSTORE_PASSWORD); supportedProperties.add(PROP_KEYSTORE_TYPE); supportedProperties.add(PROP_KEY_PASSWORD); } @Override protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { return supportedProperties; } @Override protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName){ return new PropertyDescriptor.Builder() .required(false) .name(propertyDescriptorName) .addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true)) .dynamic(true) .expressionLanguageSupported(true) .build(); } @Override protected void init(final NotificationInitializationContext context) { final String url = context.getProperty(PROP_URL).evaluateAttributeExpressions().getValue(); if (url == null || url.isEmpty()) { throw new IllegalArgumentException("Property, \"" + PROP_URL.getDisplayName() + "\", for the URL to POST notifications to must be set."); } urlReference.set(url); httpClientReference.set(null); final OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder(); Long connectTimeout = context.getProperty(PROP_CONNECTION_TIMEOUT).asTimePeriod(TimeUnit.MILLISECONDS); Long writeTimeout = context.getProperty(PROP_WRITE_TIMEOUT).asTimePeriod(TimeUnit.MILLISECONDS); // Set timeouts okHttpClientBuilder.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS); okHttpClientBuilder.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS); // check if the keystore is set and add the factory if so if (url.toLowerCase().startsWith("https")) { try { SSLSocketFactory sslSocketFactory = getSslSocketFactory(context); okHttpClientBuilder.sslSocketFactory(sslSocketFactory); } catch (Exception e) { throw new IllegalStateException(e); } } httpClientReference.set(okHttpClientBuilder.build()); } @Override public void notify(NotificationContext context, NotificationType notificationType, String subject, String message) throws NotificationFailedException { try { final RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), message); Request.Builder requestBuilder = new Request.Builder() .post(requestBody) .url(urlReference.get()); Map<PropertyDescriptor, String> configuredProperties = context.getProperties(); for(PropertyDescriptor propertyDescriptor: configuredProperties.keySet()) { if (propertyDescriptor.isDynamic()) { String propertyValue = context.getProperty(propertyDescriptor).evaluateAttributeExpressions().getValue(); requestBuilder = requestBuilder.addHeader(propertyDescriptor.getDisplayName(), propertyValue); } } final Request request = requestBuilder .addHeader(NOTIFICATION_SUBJECT_KEY, subject) .addHeader(NOTIFICATION_TYPE_KEY, notificationType.name()) .build(); final OkHttpClient httpClient = httpClientReference.get(); final Call call = httpClient.newCall(request); try (final Response response = call.execute()) { if (!response.isSuccessful()) { throw new NotificationFailedException("Failed to send Http Notification. Received an unsuccessful status code response '" + response.code() + "'. The message was '" + response.message() + "'"); } } } catch (NotificationFailedException e){ throw e; } catch (Exception e) { throw new NotificationFailedException("Failed to send Http Notification", e); } } private static SSLSocketFactory getSslSocketFactory(NotificationInitializationContext context) throws Exception { final String protocol = context.getProperty(SSL_ALGORITHM).getValue(); try { final PropertyValue keyPasswdProp = context.getProperty(PROP_KEY_PASSWORD); final char[] keyPassword = keyPasswdProp.isSet() ? keyPasswdProp.getValue().toCharArray() : null; final SSLContext sslContext; final String truststoreFile = context.getProperty(PROP_TRUSTSTORE).getValue(); final String keystoreFile = context.getProperty(PROP_KEYSTORE).getValue(); if (keystoreFile == null) { // If keystore not specified, create SSL Context based only on trust store. sslContext = SslContextFactory.createTrustSslContext( context.getProperty(PROP_TRUSTSTORE).getValue(), context.getProperty(PROP_TRUSTSTORE_PASSWORD).getValue().toCharArray(), context.getProperty(PROP_TRUSTSTORE_TYPE).getValue(), protocol); } else if (truststoreFile == null) { // If truststore not specified, create SSL Context based only on key store. sslContext = SslContextFactory.createSslContext( context.getProperty(PROP_KEYSTORE).getValue(), context.getProperty(PROP_KEYSTORE_PASSWORD).getValue().toCharArray(), keyPassword, context.getProperty(PROP_KEYSTORE_TYPE).getValue(), protocol); } else { sslContext = SslContextFactory.createSslContext( context.getProperty(PROP_KEYSTORE).getValue(), context.getProperty(PROP_KEYSTORE_PASSWORD).getValue().toCharArray(), keyPassword, context.getProperty(PROP_KEYSTORE_TYPE).getValue(), context.getProperty(PROP_TRUSTSTORE).getValue(), context.getProperty(PROP_TRUSTSTORE_PASSWORD).getValue().toCharArray(), context.getProperty(PROP_TRUSTSTORE_TYPE).getValue(), SslContextFactory.ClientAuth.REQUIRED, protocol); } return sslContext.getSocketFactory(); } catch (final Exception e) { throw new ProcessException(e); } } }