/* $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();
}
}
}