/* $Id$ */ /** * 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.manifoldcf.crawler.notifications.slack; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import javax.net.ssl.SSLSocketFactory; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.NTCredentials; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.entity.EntityBuilder; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.entity.ContentType; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.apache.manifoldcf.connectorcommon.common.InterruptibleSocketFactory; import org.apache.manifoldcf.connectorcommon.interfaces.KeystoreManagerFactory; import org.apache.manifoldcf.core.interfaces.ManifoldCFException; import org.apache.manifoldcf.crawler.system.Logging; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.ObjectMapper; /** This class represents a slack web hook session, without any protection * from threads waiting on sockets, etc. */ public class SlackSession { private static String currentHost = null; private CloseableHttpClient httpClient; private ObjectMapper objectMapper; private final String webHookUrl; static { // Find the current host name try { java.net.InetAddress addr = java.net.InetAddress.getLocalHost(); // Get hostname currentHost = addr.getHostName(); } catch (java.net.UnknownHostException e) { } } /** * Create a session. * @param webHookUrl - the webHookUrl to use for slack messages. * @param proxySettingsOrNull - the proxy settings or null if not necessary. * @throws ManifoldCFException */ public SlackSession(final String webHookUrl, final ProxySettings proxySettingsOrNull) throws ManifoldCFException { this.webHookUrl = webHookUrl; this.objectMapper = new ObjectMapper(); this.objectMapper.setSerializationInclusion(Include.NON_NULL); int connectionTimeout = 60000; int socketTimeout = 900000; final RequestConfig.Builder requestBuilder = RequestConfig.custom() .setSocketTimeout(socketTimeout) .setConnectTimeout(connectionTimeout) .setConnectionRequestTimeout(socketTimeout); if(proxySettingsOrNull != null) { addProxySettings(requestBuilder, proxySettingsOrNull); } // Create a ssl socket factory trusting everything. // Reason: manifoldcf wishes connectors to encapsulate certificate handling // per connection and not rely on the global keystore. // A configurable keystore seems overkill for the slack notification use case // so we trust everything. SSLSocketFactory httpsSocketFactory = KeystoreManagerFactory.getTrustingSecureSocketFactory(); SSLConnectionSocketFactory myFactory = new SSLConnectionSocketFactory(new InterruptibleSocketFactory(httpsSocketFactory,connectionTimeout), NoopHostnameVerifier.INSTANCE); httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(requestBuilder.build()) .setSSLSocketFactory(myFactory) .build(); } private void addProxySettings(RequestConfig.Builder requestBuilder, ProxySettings proxySettingsOrNull) { if (proxySettingsOrNull.hasUsername()) { CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( new AuthScope(proxySettingsOrNull.getHost(), proxySettingsOrNull.getPort()), new NTCredentials(proxySettingsOrNull.getUsername(), (proxySettingsOrNull.getPassword() == null) ? "" : proxySettingsOrNull.getPassword(), currentHost, (proxySettingsOrNull.getDomain() == null) ? "" : proxySettingsOrNull.getDomain())); } HttpHost proxy = new HttpHost(proxySettingsOrNull.getHost(), proxySettingsOrNull.getPort()); requestBuilder.setProxy(proxy); } public void checkConnection() throws IOException { HttpPost postRequest = new HttpPost(webHookUrl); int statusCode; String responseBody = null; try (CloseableHttpResponse response = httpClient.execute(postRequest)) { responseBody = EntityUtils.toString(response.getEntity()); statusCode = response.getStatusLine().getStatusCode(); } // the API responds with error 400 and payload "invalid_payload" // when called without proper payload. We use this // as a connection check, since there is no specific method to // check if there is a working webhook endpoint. boolean isExpectedStatus = statusCode == HttpStatus.SC_BAD_REQUEST; boolean isExpectedPayload = "invalid_payload".equals(responseBody); boolean isConnectionOk = isExpectedStatus && isExpectedPayload; if (!isConnectionOk) { String messageTemplate = "connection failed: status {0}, payload {1}"; String statusInfo = isExpectedStatus ? "ok" : statusCode + " is unexpected"; String payloadInfo = isExpectedPayload ? "ok" : "is unexpected"; String message = MessageFormat.format(messageTemplate, statusInfo, payloadInfo); throw new ClientProtocolException(message); } } public void send(String channel, String message) throws IOException { HttpPost messagePost = new HttpPost(webHookUrl); SlackMessage slackMessage = new SlackMessage(); if (StringUtils.isNotBlank(channel)) { slackMessage.setChannel(channel); } slackMessage.setText(message); String json = objectMapper.writeValueAsString(slackMessage); HttpEntity entity = EntityBuilder.create() .setContentType(ContentType.APPLICATION_JSON) .setContentEncoding(StandardCharsets.UTF_8.name()) .setText(json) .build(); messagePost.setEntity(entity); try (CloseableHttpResponse response = httpClient.execute(messagePost)) { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_CREATED) { EntityUtils.consume(response.getEntity()); } else { Logging.connectors.error("Sending slack message failed with statusline " + response.getStatusLine()); Logging.connectors.debug(" Response was: " + EntityUtils.toString(response.getEntity())); } } } public void close() throws IOException { httpClient.close(); httpClient = null; objectMapper = null; } protected static final class ProxySettings { private String host; private int port = -1; private String username; private String password; private String domain; public ProxySettings(String host, String portString, String username, String password, String domain) { this.host = host; if(StringUtils.isNotEmpty(portString)) { try { this.port = Integer.parseInt(portString); } catch (NumberFormatException e) { Logging.connectors.warn("Proxy port must be an number. Found " + portString); } } this.username = username; this.password = password; this.domain = domain; } public String getHost() { return host; } public int getPort() { return port; } public String getUsername() { return username; } public boolean hasUsername() { return StringUtils.isNotEmpty(this.username); } public String getPassword() { return password; } public String getDomain() { return domain; } @Override public String toString() { final StringBuilder sb = new StringBuilder("ProxySettings{"); sb.append("host='").append(host).append('\''); sb.append(", port=").append(port); sb.append(", username='").append(username).append('\''); sb.append(", password='").append(password).append('\''); sb.append(", domain='").append(domain).append('\''); sb.append('}'); return sb.toString(); } } }